ihp 1.4.1 → 1.5.0
raw patch · 185 files changed
+13727/−10922 lines, 185 filesdep +contravariantdep +filepathdep +hashabledep −ihp-postgresql-simple-extradep −ipdep −mime-maildep ~directory
Dependencies added: contravariant, filepath, hashable, hasql, hasql-dynamic-statements, hasql-implicits, hasql-mapping, hasql-pool, hasql-postgresql-types, hasql-transaction, ihp, ihp-context, ihp-imagemagick, ihp-log, ihp-modal, ihp-pagehead, ihp-pglistener, postgresql-simple-postgresql-types, postgresql-types, slugger, stm, tasty-bench, wai-asset-path, wai-flash-messages, wai-request-params, wai-session-clientsession-deferred, wai-session-maybe
Dependencies removed: ihp-postgresql-simple-extra, ip, mime-mail, mime-mail-ses, smtp-mail, wai-session, wai-session-clientsession
Dependency ranges changed: directory
Files
- CHANGELOG.md +4/−8
- IHP/ActionType.hs +60/−0
- IHP/ApplicationContext.hs +0/−13
- IHP/Assets/Types.hs +0/−17
- IHP/Assets/ViewFunctions.hs +0/−37
- IHP/AuthSupport/Controller/Sessions.hs +14/−6
- IHP/AutoRefresh.hs +177/−104
- IHP/AutoRefresh/Types.hs +7/−6
- IHP/AutoRefresh/View.hs +7/−3
- IHP/Breadcrumb/Types.hs +6/−6
- IHP/Breadcrumb/ViewFunctions.hs +1/−2
- IHP/Controller/AccessDenied.hs +6/−6
- IHP/Controller/BasicAuth.hs +1/−1
- IHP/Controller/Context.hs +50/−122
- IHP/Controller/Cookie.hs +3/−3
- IHP/Controller/FileUpload.hs +11/−10
- IHP/Controller/Layout.hs +43/−9
- IHP/Controller/NotFound.hs +6/−6
- IHP/Controller/Param.hs +89/−338
- IHP/Controller/Redirect.hs +46/−16
- IHP/Controller/Render.hs +67/−66
- IHP/Controller/RequestContext.hs +0/−26
- IHP/Controller/Response.hs +24/−11
- IHP/Controller/Session.hs +22/−19
- IHP/ControllerPrelude.hs +17/−10
- IHP/ControllerSupport.hs +127/−90
- IHP/DataSync/ChangeNotifications.hs +0/−187
- IHP/DataSync/Controller.hs +0/−26
- IHP/DataSync/ControllerImpl.hs +0/−533
- IHP/DataSync/DynamicQuery.hs +0/−221
- IHP/DataSync/DynamicQueryCompiler.hs +0/−190
- IHP/DataSync/REST/Controller.hs +0/−239
- IHP/DataSync/REST/Routes.hs +0/−50
- IHP/DataSync/REST/Types.hs +0/−12
- IHP/DataSync/Role.hs +0/−69
- IHP/DataSync/RowLevelSecurity.hs +0/−138
- IHP/DataSync/Types.hs +0/−77
- IHP/EnvVar.hs +11/−14
- IHP/Environment.hs +1/−1
- IHP/ErrorController.hs +111/−95
- IHP/Fetch.hs +52/−104
- IHP/Fetch/Statement.hs +56/−0
- IHP/FetchPipelined.hs +174/−0
- IHP/FetchRelated.hs +14/−14
- IHP/FileStorage/ControllerFunctions.hs +13/−5
- IHP/FileStorage/Preprocessor/ImageMagick.hs +0/−65
- IHP/FlashMessages.hs +95/−0
- IHP/FlashMessages/ControllerFunctions.hs +0/−70
- IHP/FlashMessages/Types.hs +0/−13
- IHP/FlashMessages/ViewFunctions.hs +0/−41
- IHP/FrameworkConfig.hs +29/−256
- IHP/FrameworkConfig/Types.hs +161/−0
- IHP/HaskellSupport.hs +16/−171
- IHP/Hasql/Encoders.hs +258/−0
- IHP/Hasql/FromRow.hs +88/−0
- IHP/Hasql/Pool.hs +48/−0
- IHP/InputValue.hs +92/−0
- IHP/Job/Dashboard.hs +0/−400
- IHP/Job/Dashboard/Auth.hs +0/−52
- IHP/Job/Dashboard/Types.hs +0/−112
- IHP/Job/Dashboard/Utils.hs +0/−15
- IHP/Job/Dashboard/View.hs +0/−305
- IHP/Job/Queue.hs +43/−268
- IHP/Job/Queue/Fetch.hs +61/−0
- IHP/Job/Queue/Pool.hs +12/−0
- IHP/Job/Queue/Result.hs +156/−0
- IHP/Job/Queue/STM.hs +18/−0
- IHP/Job/Queue/StatusInstances.hs +83/−0
- IHP/Job/Queue/Watch.hs +176/−0
- IHP/Job/Runner.hs +20/−196
- IHP/Job/Runner/MainLoop.hs +123/−0
- IHP/Job/Runner/WorkerLoop.hs +139/−0
- IHP/Job/Types.hs +4/−72
- IHP/Job/Types/BackoffStrategy.hs +10/−0
- IHP/Job/Types/Class.hs +41/−0
- IHP/Job/Types/Status.hs +17/−0
- IHP/Job/Types/Worker.hs +39/−0
- IHP/Log.hs +0/−122
- IHP/Log/Types.hs +0/−252
- IHP/LoginSupport/Helper/Controller.hs +20/−20
- IHP/LoginSupport/Middleware.hs +3/−1
- IHP/Mail.hs +0/−153
- IHP/Mail/Types.hs +0/−41
- IHP/MailPrelude.hs +0/−16
- IHP/Modal/ControllerFunctions.hs +0/−21
- IHP/Modal/Types.hs +0/−21
- IHP/Modal/ViewFunctions.hs +0/−56
- IHP/ModelSupport.hs +349/−474
- IHP/ModelSupport/Types.hs +248/−0
- IHP/NameSupport.hs +2/−12
- IHP/PGListener.hs +0/−274
- IHP/PGSimpleCompat.hs +66/−0
- IHP/PGVersion.hs +14/−0
- IHP/PageHead/ControllerFunctions.hs +0/−57
- IHP/PageHead/Types.hs +0/−21
- IHP/PageHead/ViewFunctions.hs +0/−261
- IHP/Pagination/ControllerFunctions.hs +27/−15
- IHP/Pagination/Helpers.hs +1/−1
- IHP/Pagination/Types.hs +2/−1
- IHP/Pagination/ViewFunctions.hs +14/−14
- IHP/Prelude.hs +37/−3
- IHP/QueryBuilder.hs +108/−1192
- IHP/QueryBuilder/Compiler.hs +89/−0
- IHP/QueryBuilder/Filter.hs +633/−0
- IHP/QueryBuilder/HasqlCompiler.hs +180/−0
- IHP/QueryBuilder/HasqlHelpers.hs +37/−0
- IHP/QueryBuilder/Join.hs +107/−0
- IHP/QueryBuilder/Order.hs +179/−0
- IHP/QueryBuilder/Types.hs +300/−0
- IHP/QueryBuilder/Union.hs +96/−0
- IHP/Record.hs +99/−0
- IHP/RequestVault.hs +57/−0
- IHP/RequestVault/Helper.hs +40/−0
- IHP/RequestVault/ModelContext.hs +43/−0
- IHP/Router/Types.hs +23/−1
- IHP/Router/UrlGenerator.hs +29/−0
- IHP/RouterPrelude.hs +1/−0
- IHP/RouterSupport.hs +480/−297
- IHP/SchemaMigration.hs +0/−136
- IHP/ScriptSupport.hs +26/−10
- IHP/Server.hs +110/−47
- IHP/Static.hs +21/−0
- IHP/Test/Mocking.hs +136/−49
- IHP/ValidationSupport/Types.hs +9/−2
- IHP/ValidationSupport/ValidateField.hs +1/−0
- IHP/ValidationSupport/ValidateIsUnique.hs +9/−9
- IHP/View/CSSFramework.hs +9/−1125
- IHP/View/CSSFramework/Bootstrap.hs +307/−0
- IHP/View/CSSFramework/Tailwind.hs +333/−0
- IHP/View/CSSFramework/Unstyled.hs +401/−0
- IHP/View/Classes.hs +5/−1
- IHP/View/Form.hs +9/−945
- IHP/View/Form/Fields.hs +521/−0
- IHP/View/Form/FormFor.hs +354/−0
- IHP/View/Form/Select.hs +182/−0
- IHP/View/TimeAgo.hs +5/−1
- IHP/View/Types.hs +10/−4
- IHP/ViewErrorMessages.hs +0/−8
- IHP/ViewPrelude.hs +10/−6
- IHP/ViewSupport.hs +41/−59
- IHP/WebSocket.hs +7/−7
- IHP/Welcome/Controller.hs +0/−65
- Test/RouterBench.hs +129/−0
- Test/Test/AutoRefreshSpec.hs +188/−0
- Test/Test/Controller/AccessDeniedSpec.hs +70/−0
- Test/Test/Controller/ContextSpec.hs +112/−0
- Test/Test/Controller/CookieSpec.hs +47/−0
- Test/Test/Controller/NotFoundSpec.hs +70/−0
- Test/Test/Controller/ParamSpec.hs +506/−0
- Test/Test/ControllerSupportSpec.hs +118/−0
- Test/Test/FetchPipelinedSpec.hs +147/−0
- Test/Test/FileStorage/ControllerFunctionsSpec.hs +101/−0
- Test/Test/HaskellSupportSpec.hs +59/−0
- Test/Test/HasqlEncoderSpec.hs +112/−0
- Test/Test/JobQueueSpec.hs +148/−0
- Test/Test/Main.hs +54/−0
- Test/Test/MockingSpec.hs +70/−0
- Test/Test/ModelFixtures.hs +146/−0
- Test/Test/ModelSupportSpec.hs +141/−0
- Test/Test/NameSupportSpec.hs +115/−0
- Test/Test/PGListenerSpec.hs +45/−0
- Test/Test/Pagination/ControllerFunctionsSpec.hs +160/−0
- Test/Test/QueryBuilderSpec.hs +535/−0
- Test/Test/RouterSupportSpec.hs +251/−0
- Test/Test/ValidationSupport/ValidateFieldSpec.hs +212/−0
- Test/Test/View/CSSFrameworkSpec.hs +729/−0
- Test/Test/View/FormSpec.hs +194/−0
- Test/Test/ViewSupportSpec.hs +111/−0
- data/static/helpers.js +21/−10
- data/static/vendor/bootstrap-5.2.1/bootstrap.min.css +0/−7
- data/static/vendor/bootstrap-5.2.1/bootstrap.min.js +0/−7
- data/static/vendor/bootstrap.min.css +0/−7
- data/static/vendor/bootstrap.min.css.map +0/−1
- data/static/vendor/bootstrap.min.js +0/−7
- data/static/vendor/bootstrap.min.js.map +0/−1
- data/static/vendor/ihp-ssc.js +276/−29
- data/static/vendor/jquery-3.2.1.slim.min.js +0/−4
- data/static/vendor/jquery-3.5.0.min.js +0/−2
- data/static/vendor/jquery-3.6.0.min.js +0/−2
- data/static/vendor/jquery-3.6.0.slim.min.js +0/−2
- data/static/vendor/popper.min.js +0/−5
- data/static/vendor/popper.min.js.map +0/−1
- data/static/vendor/select2.min.css +0/−1
- data/static/vendor/select2.min.js +0/−2
- ihp.cabal +121/−88
@@ -1,12 +1,8 @@-# v1.4.1--- ihp-{openai,sitemap}: increase base bounds from 4.20 => 4.22-- fix migrate package-- use Data.Text qualified in IHP.NameSupport to fix build with text-2.1.2-- Drop 'Default Bool' instance for data-default >= 0.8--# v1.4.0+# unreleased+- [Removed option: 'WORKING_DIRECTORY' no longer required on deployment static assets wrapped automatically](https://github.com/digitallyinduced/ihp/commit/9c54588d983d74bdc28b063d1132952deb57dc16)+- Experimental GHC 9.12 support via `pkgs.ghc912` overlay attribute ([#2264](https://github.com/digitallyinduced/ihp/pull/2264)) +# v1.4.0 (unreleased) ## Highlights - 🚀 Improved Dev Server: faster, more reliable reloads without race conditions. - 📦 Ecosystem split: IHP now modular (`ihp-ide`, `ihp-ssc`, `ihp-migrate`, `ihp-sitemap`).
@@ -0,0 +1,60 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-|+Module: IHP.ActionType+Copyright: (c) digitally induced GmbH, 2025++Provides ActionType for tracking the current controller action type+in the WAI request vault.+-}+module IHP.ActionType+( ActionType(..)+, actionTypeVaultKey+, requestActionType+, setActionType+, isActiveController+) where++import Prelude+import GHC.Records (HasField(..))+import Data.Typeable (Typeable)+import Network.Wai+import System.IO.Unsafe (unsafePerformIO)+import qualified Data.Vault.Lazy as Vault+import qualified Data.Typeable as Typeable+import Data.Proxy++-- | Used to track the current action type+newtype ActionType = ActionType Typeable.TypeRep++actionTypeVaultKey :: Vault.Key ActionType+actionTypeVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE actionTypeVaultKey #-}++requestActionType :: Request -> ActionType+requestActionType req =+ case Vault.lookup actionTypeVaultKey req.vault of+ Just actionType -> actionType+ Nothing -> error "requestActionType: ActionType not found in request vault"++-- | Insert the ActionType into the request vault+{-# INLINE setActionType #-}+setActionType :: Typeable controller => controller -> Request -> Request+setActionType controller req = req { vault = Vault.insert actionTypeVaultKey (ActionType (Typeable.typeOf controller)) req.vault }++instance HasField "actionType" Request ActionType where+ getField = requestActionType++-- | Returns @True@ when the given type matches the type of the currently executed controller action+--+-- __Example:__ The browser has requested @\/Posts@ and the @Posts@ action of the @PostsController@ is called.+--+-- >>> isActiveController @PostsController+-- True+--+-- Returns @True@ because the current action is part of the @PostsController@+isActiveController :: forall controller. (?request :: Request, Typeable controller) => Bool+isActiveController =+ let+ (ActionType actionType) = ?request.actionType+ in+ (Typeable.typeRep (Proxy @controller)) == actionType
@@ -1,13 +0,0 @@-module IHP.ApplicationContext where--import IHP.Prelude-import IHP.AutoRefresh.Types (AutoRefreshServer)-import IHP.FrameworkConfig (FrameworkConfig)-import IHP.PGListener (PGListener)--data ApplicationContext = ApplicationContext- { modelContext :: !ModelContext- , autoRefreshServer :: !(IORef AutoRefreshServer)- , frameworkConfig :: !FrameworkConfig- , pgListener :: PGListener- }
@@ -1,17 +0,0 @@-{-|-Module: IHP.Assets.Types-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.Assets.Types-( AssetVersion (..)-) where--import IHP.Prelude---- | Stores the version appended to CSS and JS files via the @?v=..@ query parameter------ __Example:__ An asset path where the @AssetVersion@ was set to @"dev"@------ > "/app.css?v=dev"--- -newtype AssetVersion = AssetVersion Text
@@ -1,37 +0,0 @@-{-|-Module: IHP.Assets.ViewFunctions-Description: Functions for dealing with CSS and JS files, e.g. 'assetPath'-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.Assets.ViewFunctions-( assetPath-, assetVersion-) where--import IHP.Prelude-import IHP.Controller.Context-import qualified IHP.FrameworkConfig as Config---- | Adds a cache buster to a asset path------ >>> assetPath "/keyhandlers.js"--- "/keyhandlers.js?v=9be8995c-7055-43d9-a1b2-43e05c210271"------ The asset version can be configured using the--- @IHP_ASSET_VERSION@ environment variable.-assetPath :: (?context :: ControllerContext) => Text -> Text-assetPath assetPath = baseUrl <> assetPath <> "?v=" <> assetVersion- where- baseUrl = fromMaybe "" ?context.frameworkConfig.assetBaseUrl-{-# INLINABLE assetPath #-}---- | Returns the assetVersion------ >>> assetVersion--- "9be8995c-7055-43d9-a1b2-43e05c210271"------ The asset version can be configured using the--- @IHP_ASSET_VERSION@ environment variable.-assetVersion :: (?context :: ControllerContext) => Text-assetVersion = ?context.frameworkConfig.assetVersion-{-# INLINABLE assetVersion #-}
@@ -19,6 +19,7 @@ import Data.Data import qualified IHP.AuthSupport.Lockable as Lockable import System.IO.Unsafe (unsafePerformIO)+import IHP.Hasql.FromRow (FromRowHasql) -- | Displays the login form. --@@ -26,6 +27,8 @@ newSessionAction :: forall record action. ( ?theAction :: action , ?context :: ControllerContext+ , ?request :: Request+ , ?respond :: Respond , HasNewSessionUrl record , ?modelContext :: ModelContext , Typeable record@@ -37,7 +40,7 @@ ) => IO () newSessionAction = do let alreadyLoggedIn = isJust (currentUserOrNothing @record)- when alreadyLoggedIn (redirectToPath (afterLoginRedirectPath @record))+ when alreadyLoggedIn (redirectToPathSeeOther (afterLoginRedirectPath @record)) let user = newRecord @record render NewView { .. }@@ -51,6 +54,8 @@ createSessionAction :: forall record action. (?theAction :: action , ?context :: ControllerContext+ , ?request :: Request+ , ?respond :: Respond , ?modelContext :: ModelContext , Data action , HasField "email" record Text@@ -65,6 +70,7 @@ , Show (PrimaryKey (GetTableName record)) , record ~ GetModelByTableName (GetTableName record) , Table record+ , FromRowHasql record ) => IO () createSessionAction = do usersQueryBuilder@@ -85,7 +91,7 @@ |> set #failedLoginAttempts 0 |> updateRecord redirectUrl <- getSessionAndClear "IHP.LoginSupport.redirectAfterLogin"- redirectToPath (fromMaybe (afterLoginRedirectPath @record) redirectUrl)+ redirectToPathSeeOther (fromMaybe (afterLoginRedirectPath @record) redirectUrl) else do setErrorMessage "Invalid Credentials" user :: record <- user@@ -104,6 +110,8 @@ deleteSessionAction :: forall record action id. ( ?theAction :: action , ?context :: ControllerContext+ , ?request :: Request+ , ?respond :: Respond , ?modelContext :: ModelContext , Data action , HasPath action@@ -117,7 +125,7 @@ beforeLogout user logout user Nothing -> pure ()- redirectToPath (afterLogoutRedirectPath @record)+ redirectToPathSeeOther (afterLogoutRedirectPath @record) {-# INLINE deleteSessionAction #-} currentUserOrNothing :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user) => (Maybe user)@@ -147,7 +155,7 @@ , KnownSymbol (GetModelName record) , HasNewSessionUrl record , KnownSymbol (GetTableName record)- , FromRow record+ , FromRowHasql record ) => SessionsControllerConfig record where -- | Your home page, where the user is redirect after login, by default it's @/@@@ -172,13 +180,13 @@ -- > unless (user.isConfirmed) do -- > setErrorMessage "Please click the confirmation link we sent to your email before you can use the App" -- > redirectTo NewSessionAction- beforeLogin :: (?context :: ControllerContext, ?modelContext :: ModelContext) => record -> IO ()+ beforeLogin :: (?context :: ControllerContext, ?modelContext :: ModelContext, ?request :: Request) => record -> IO () beforeLogin _ = pure () -- | Callback that is executed just before the user is logged out -- -- This is called only if user session exists- beforeLogout :: (?context :: ControllerContext, ?modelContext :: ModelContext) => record -> IO ()+ beforeLogout :: (?context :: ControllerContext, ?modelContext :: ModelContext, ?request :: Request) => record -> IO () beforeLogout _ = pure () -- | Return's the @query\ \@User@ used by the controller. Customize this to e.g. exclude guest users from logging in.
@@ -8,7 +8,6 @@ import IHP.Prelude import IHP.AutoRefresh.Types import IHP.ControllerSupport-import IHP.ApplicationContext import qualified Data.UUID.V4 as UUID import qualified Data.UUID as UUID import IHP.Controller.Session@@ -22,87 +21,119 @@ import qualified Data.Text as Text import IHP.WebSocket import IHP.Controller.Context-import IHP.Controller.Response +import IHP.Controller.Response import qualified IHP.PGListener as PGListener-import qualified Database.PostgreSQL.Simple.Types as PG-import Data.String.Interpolate.IsString+import qualified Hasql.Session as HasqlSession+import qualified IHP.Log as Log+import qualified Data.Vault.Lazy as Vault+import System.IO.Unsafe (unsafePerformIO)+import Network.Wai+import qualified Data.TMap as TypeMap+import IHP.RequestVault (pgListenerVaultKey)+import IHP.FrameworkConfig.Types (FrameworkConfig(..))+import IHP.Environment (Environment(..)) -initAutoRefresh :: (?context :: ControllerContext, ?applicationContext :: ApplicationContext) => IO ()-initAutoRefresh = do- putContext AutoRefreshDisabled- putContext ?applicationContext.autoRefreshServer+{-# NOINLINE globalAutoRefreshServerVar #-}+globalAutoRefreshServerVar :: MVar.MVar (Maybe (IORef AutoRefreshServer))+globalAutoRefreshServerVar = unsafePerformIO (MVar.newMVar Nothing) +getOrCreateAutoRefreshServer :: (?request :: Request) => IO (IORef AutoRefreshServer)+getOrCreateAutoRefreshServer =+ MVar.modifyMVar globalAutoRefreshServerVar $ \case+ Just server -> pure (Just server, server)+ Nothing -> do+ let pgListener = case Vault.lookup pgListenerVaultKey ?request.vault of+ Just pl -> pl+ Nothing -> error "getOrCreateAutoRefreshServer: PGListener not found in request vault"+ server <- newIORef (newAutoRefreshServer pgListener)+ pure (Just server, server)+ autoRefresh :: ( ?theAction :: action , Controller action , ?modelContext :: ModelContext , ?context :: ControllerContext+ , ?request :: Request ) => ((?modelContext :: ModelContext) => IO ()) -> IO () autoRefresh runAction = do- autoRefreshState <- fromContext @AutoRefreshState- autoRefreshServer <- fromContext @(IORef AutoRefreshServer)+ -- When PGListener is not available, degrade gracefully to a+ -- plain action without auto-refresh.+ case Vault.lookup pgListenerVaultKey ?request.vault of+ Nothing -> runAction+ Just _ -> do+ let autoRefreshState = Vault.lookup autoRefreshStateVaultKey ?request.vault+ autoRefreshServer <- getOrCreateAutoRefreshServer - case autoRefreshState of- AutoRefreshDisabled -> do- availableSessions <- getAvailableSessions autoRefreshServer+ case autoRefreshState of+ Just (AutoRefreshEnabled {}) -> do+ -- When this function calls the 'action ?theAction' in the other case+ -- we will evaluate this branch+ runAction+ _ -> do+ availableSessions <- getAvailableSessions autoRefreshServer - id <- UUID.nextRandom+ id <- UUID.nextRandom - -- We save the current state of the controller context here. This includes e.g. all current- -- flash messages, the current user, ...- --- -- This frozen context is used as a "template" inside renderView to make a new controller context- -- with the exact same content we had when rendering the initial page, whenever we do a server-side re-rendering- frozenControllerContext <- freeze ?context+ -- Update the vault with AutoRefreshEnabled so that autoRefreshMeta can read it+ let newRequest = ?request { vault = Vault.insert autoRefreshStateVaultKey (AutoRefreshEnabled id) ?request.vault }+ let ?request = newRequest+ -- Update request in controller context so freeze captures the updated state+ let ControllerContext { customFieldsRef } = ?context+ modifyIORef' customFieldsRef (TypeMap.insert @Network.Wai.Request newRequest) - let renderView = \requestContext -> do- controllerContext <- unfreeze frozenControllerContext- let ?context = controllerContext { requestContext }- action ?theAction+ -- We save the current state of the controller context here. This includes e.g. all current+ -- flash messages, the current user, ...+ --+ -- This frozen context is used as a "template" inside renderView to make a new controller context+ -- with the exact same content we had when rendering the initial page, whenever we do a server-side re-rendering+ frozenControllerContext <- freeze ?context - putContext (AutoRefreshEnabled id)+ let originalRequest = ?request+ let renderView = \_waiRequest waiRespond -> do+ controllerContext <- unfreeze frozenControllerContext+ let ?context = controllerContext+ let ?request = originalRequest+ let ?respond = waiRespond+ putContext originalRequest+ action ?theAction - -- We save the allowed session ids to the session cookie to only grant a client access- -- to sessions it initially opened itself- --- -- Otherwise you might try to guess session UUIDs to access other peoples auto refresh sessions- setSession "autoRefreshSessions" (map UUID.toText (id:availableSessions) |> Text.intercalate "")+ -- We save the allowed session ids to the session cookie to only grant a client access+ -- to sessions it initially opened itself+ --+ -- Otherwise you might try to guess session UUIDs to access other peoples auto refresh sessions+ setSession "autoRefreshSessions" (map UUID.toText (id:availableSessions) |> Text.intercalate "") - withTableReadTracker do- let handleResponse exception@(ResponseException response) = case response of- Wai.ResponseBuilder status headers builder -> do- tables <- readIORef ?touchedTables- lastPing <- getCurrentTime+ withTableReadTracker do+ let handleResponse exception@(ResponseException response) = case response of+ Wai.ResponseBuilder status headers builder -> do+ tables <- readIORef ?touchedTables+ lastPing <- getCurrentTime - -- It's important that we evaluate the response to HNF here- -- Otherwise a response `error "fail"` will break auto refresh and cause- -- the action to be unreachable until the server is restarted.- --- -- Specifically a request like this will crash the action:- --- -- > curl --header 'Accept: application/json' http://localhost:8000/ShowItem?itemId=6bbe1a72-400a-421e-b26a-ff58d17af3e5- --- -- Let's assume that the view has no implementation for JSON responses. Then- -- it will render a 'error "JSON not implemented"'. After this curl request- -- all future HTML requests to the current action will fail with a 503.- --- lastResponse <- Exception.evaluate (ByteString.toLazyByteString builder)+ -- It's important that we evaluate the response to HNF here+ -- Otherwise a response `error "fail"` will break auto refresh and cause+ -- the action to be unreachable until the server is restarted.+ --+ -- Specifically a request like this will crash the action:+ --+ -- > curl --header 'Accept: application/json' http://localhost:8000/ShowItem?itemId=6bbe1a72-400a-421e-b26a-ff58d17af3e5+ --+ -- Let's assume that the view has no implementation for JSON responses. Then+ -- it will render a 'error "JSON not implemented"'. After this curl request+ -- all future HTML requests to the current action will fail with a 503.+ --+ lastResponse <- Exception.evaluate (ByteString.toLazyByteString builder) - event <- MVar.newEmptyMVar- let session = AutoRefreshSession { id, renderView, event, tables, lastResponse, lastPing }- modifyIORef' autoRefreshServer (\s -> s { sessions = session:s.sessions } )- async (gcSessions autoRefreshServer)+ event <- MVar.newEmptyMVar+ let session = AutoRefreshSession { id, renderView, event, tables, lastResponse, lastPing }+ modifyIORef' autoRefreshServer (\s -> s { sessions = session:s.sessions } )+ async (gcSessions autoRefreshServer) - registerNotificationTrigger ?touchedTables autoRefreshServer+ registerNotificationTrigger ?touchedTables autoRefreshServer - throw exception- _ -> error "Unimplemented WAI response type."+ throw exception+ _ -> error "Unimplemented WAI response type." - runAction `Exception.catch` handleResponse- AutoRefreshEnabled {} -> do- -- When this function calls the 'action ?theAction' in the other case- -- we will evaluate this branch- runAction+ runAction `Exception.catch` handleResponse data AutoRefreshWSApp = AwaitingSessionID | AutoRefreshActive { sessionId :: UUID } instance WSApp AutoRefreshWSApp where@@ -112,24 +143,32 @@ sessionId <- receiveData @UUID setState AutoRefreshActive { sessionId } - availableSessions <- getAvailableSessions ?applicationContext.autoRefreshServer+ autoRefreshServer <- getOrCreateAutoRefreshServer+ availableSessions <- getAvailableSessions autoRefreshServer when (sessionId `elem` availableSessions) do- AutoRefreshSession { renderView, event, lastResponse } <- getSessionById sessionId+ AutoRefreshSession { renderView, event } <- getSessionById autoRefreshServer sessionId let handleResponseException (ResponseException response) = case response of- Wai.ResponseBuilder status headers builder -> do+ Wai.ResponseBuilder _status _headers builder -> do let html = ByteString.toLazyByteString builder+ responseChanged <- sessionResponseHasChanged autoRefreshServer sessionId html - when (html /= lastResponse) do+ when responseChanged do sendTextData html- updateSession sessionId (\session -> session { lastResponse = html })+ updateSession autoRefreshServer sessionId (\session -> session { lastResponse = html }) _ -> error "Unimplemented WAI response type." + let handleOtherException :: SomeException -> IO ()+ handleOtherException ex = Log.error ("AutoRefresh: Failed to re-render view: " <> tshow ex)+ async $ forever do MVar.takeMVar event- let requestContext = ?context.requestContext- (renderView requestContext) `catch` handleResponseException+ let currentRequest = ?request+ -- Create a dummy respond function that does nothing, since actual response+ -- is handled by the handleResponseException handler+ let dummyRespond _ = error "AutoRefresh: respond should not be called directly"+ ((renderView currentRequest dummyRespond) `catch` handleResponseException) `catch` handleOtherException pure () pure ()@@ -140,32 +179,39 @@ onPing = do now <- getCurrentTime AutoRefreshActive { sessionId } <- getState- updateSession sessionId (\session -> session { lastPing = now })+ autoRefreshServer <- getOrCreateAutoRefreshServer+ updateSession autoRefreshServer sessionId (\session -> session { lastPing = now }) onClose = do getState >>= \case AutoRefreshActive { sessionId } -> do- let autoRefreshServer = ?applicationContext.autoRefreshServer+ autoRefreshServer <- getOrCreateAutoRefreshServer modifyIORef' autoRefreshServer (\server -> server { sessions = filter (\AutoRefreshSession { id } -> id /= sessionId) server.sessions }) AwaitingSessionID -> pure () -registerNotificationTrigger :: (?modelContext :: ModelContext) => IORef (Set ByteString) -> IORef AutoRefreshServer -> IO ()+registerNotificationTrigger :: (?modelContext :: ModelContext, ?context :: ControllerContext) => IORef (Set Text) -> IORef AutoRefreshServer -> IO () registerNotificationTrigger touchedTablesVar autoRefreshServer = do touchedTables <- Set.toList <$> readIORef touchedTablesVar subscribedTables <- (.subscribedTables) <$> (autoRefreshServer |> readIORef) let subscriptionRequired = touchedTables |> filter (\table -> subscribedTables |> Set.notMember table)++ -- In development, always re-run trigger SQL for all touched tables because+ -- `make db` drops and recreates the database, destroying triggers that were+ -- previously installed. The trigger SQL is idempotent so re-running is safe.+ -- In production, only install triggers for newly seen tables.+ let isDevelopment = ?context.frameworkConfig.environment == Development+ modifyIORef' autoRefreshServer (\server -> server { subscribedTables = server.subscribedTables <> Set.fromList subscriptionRequired }) pgListener <- (.pgListener) <$> readIORef autoRefreshServer- subscriptions <- subscriptionRequired |> mapM (\table -> do- let createTriggerSql = notificationTrigger table-+ subscriptions <- subscriptionRequired |> mapM (\table -> do -- We need to add the trigger from the main IHP database role other we will get this error: -- ERROR: permission denied for schema public withRowLevelSecurityDisabled do- sqlExec createTriggerSql ()+ let pool = ?modelContext.hasqlPool+ runSessionHasql pool (HasqlSession.script (notificationTriggerSQL table)) pgListener |> PGListener.subscribe (channelName table) \notification -> do sessions <- (.sessions) <$> readIORef autoRefreshServer@@ -174,11 +220,20 @@ |> map (\session -> session.event) |> mapM (\event -> MVar.tryPutMVar event ()) pure ())++ -- Re-run trigger SQL for already-subscribed tables in dev mode+ when isDevelopment do+ let alreadySubscribed = touchedTables |> filter (\table -> subscribedTables |> Set.member table)+ forM_ alreadySubscribed \table -> do+ withRowLevelSecurityDisabled do+ let pool = ?modelContext.hasqlPool+ runSessionHasql pool (HasqlSession.script (notificationTriggerSQL table))+ modifyIORef' autoRefreshServer (\s -> s { subscriptions = s.subscriptions <> subscriptions }) pure () -- | Returns the ids of all sessions available to the client based on what sessions are found in the session cookie-getAvailableSessions :: (?context :: ControllerContext) => IORef AutoRefreshServer -> IO [UUID]+getAvailableSessions :: (?request :: Request) => IORef AutoRefreshServer -> IO [UUID] getAvailableSessions autoRefreshServer = do allSessions <- (.sessions) <$> readIORef autoRefreshServer text <- fromMaybe "" <$> getSession "autoRefreshSessions"@@ -191,22 +246,32 @@ |> pure -- | Returns a session for a given session id. Errors in case the session does not exist.-getSessionById :: (?applicationContext :: ApplicationContext) => UUID -> IO AutoRefreshSession-getSessionById sessionId = do- autoRefreshServer <- readIORef ?applicationContext.autoRefreshServer+getSessionById :: IORef AutoRefreshServer -> UUID -> IO AutoRefreshSession+getSessionById autoRefreshServer sessionId = do+ autoRefreshServer <- readIORef autoRefreshServer autoRefreshServer.sessions |> find (\AutoRefreshSession { id } -> id == sessionId) |> Maybe.fromMaybe (error "getSessionById: Could not find the session") |> pure -- | Applies a update function to a session specified by its session id-updateSession :: (?applicationContext :: ApplicationContext) => UUID -> (AutoRefreshSession -> AutoRefreshSession) -> IO ()-updateSession sessionId updateFunction = do- let server = ?applicationContext.autoRefreshServer+updateSession :: IORef AutoRefreshServer -> UUID -> (AutoRefreshSession -> AutoRefreshSession) -> IO ()+updateSession server sessionId updateFunction = do let updateSession' session = if session.id == sessionId then updateFunction session else session modifyIORef' server (\server -> server { sessions = map updateSession' server.sessions }) pure () +-- | Returns 'True' when the rendered html differs from the session's latest+-- known response.+--+-- This must read the current session state instead of comparing against a+-- websocket-local snapshot, otherwise switching back to an earlier DOM state+-- can be incorrectly suppressed as "unchanged".+sessionResponseHasChanged :: IORef AutoRefreshServer -> UUID -> LByteString -> IO Bool+sessionResponseHasChanged autoRefreshServer sessionId html = do+ currentLastResponse <- (.lastResponse) <$> getSessionById autoRefreshServer sessionId+ pure (html /= currentLastResponse)+ -- | Removes all expired sessions -- -- This is useful to avoid dead sessions hanging around. This can happen when a websocket connection was never established@@ -222,32 +287,40 @@ isSessionExpired now AutoRefreshSession { lastPing } = (now `diffUTCTime` lastPing) > (secondsToNominalDiffTime 60) -- | Returns the event name of the event that the pg notify trigger dispatches-channelName :: ByteString -> ByteString-channelName tableName = "ar_did_change_" <> tableName---- | Returns the sql code to set up a database trigger-notificationTrigger :: ByteString -> PG.Query-notificationTrigger tableName = PG.Query [i|- BEGIN;- CREATE OR REPLACE FUNCTION #{functionName}() RETURNS TRIGGER AS $$- BEGIN- PERFORM pg_notify('#{channelName tableName}', '');- RETURN new;- END;- $$ language plpgsql;- DROP TRIGGER IF EXISTS #{insertTriggerName} ON #{tableName};- CREATE TRIGGER #{insertTriggerName} AFTER INSERT ON "#{tableName}" FOR EACH STATEMENT EXECUTE PROCEDURE #{functionName}();- - DROP TRIGGER IF EXISTS #{updateTriggerName} ON #{tableName};- CREATE TRIGGER #{updateTriggerName} AFTER UPDATE ON "#{tableName}" FOR EACH STATEMENT EXECUTE PROCEDURE #{functionName}();+channelName :: Text -> ByteString+channelName tableName = "ar_did_change_" <> cs tableName - DROP TRIGGER IF EXISTS #{deleteTriggerName} ON #{tableName};- CREATE TRIGGER #{deleteTriggerName} AFTER DELETE ON "#{tableName}" FOR EACH STATEMENT EXECUTE PROCEDURE #{functionName}();- - COMMIT;- |]+-- | Returns a SQL script to set up database notification triggers.+--+-- Wrapped in a DO $$ block with EXCEPTION handler because concurrent requests+-- can race to CREATE OR REPLACE the same function, causing PostgreSQL to throw+-- 'tuple concurrently updated' (SQLSTATE XX000). This is safe to ignore: the+-- other connection's CREATE OR REPLACE will have succeeded.+notificationTriggerSQL :: Text -> Text+notificationTriggerSQL tableName =+ "DO $$\n"+ <> "BEGIN\n"+ <> " CREATE OR REPLACE FUNCTION " <> functionName <> "() RETURNS TRIGGER AS $BODY$"+ <> "BEGIN\n"+ <> " PERFORM pg_notify('" <> cs (channelName tableName) <> "', '');\n"+ <> " RETURN new;\n"+ <> "END;\n"+ <> "$BODY$ language plpgsql;\n"+ <> " DROP TRIGGER IF EXISTS " <> insertTriggerName <> " ON " <> tableName <> ";\n"+ <> " CREATE TRIGGER " <> insertTriggerName <> " AFTER INSERT ON \"" <> tableName <> "\" FOR EACH STATEMENT EXECUTE PROCEDURE " <> functionName <> "();\n"+ <> " DROP TRIGGER IF EXISTS " <> updateTriggerName <> " ON " <> tableName <> ";\n"+ <> " CREATE TRIGGER " <> updateTriggerName <> " AFTER UPDATE ON \"" <> tableName <> "\" FOR EACH STATEMENT EXECUTE PROCEDURE " <> functionName <> "();\n"+ <> " DROP TRIGGER IF EXISTS " <> deleteTriggerName <> " ON " <> tableName <> ";\n"+ <> " CREATE TRIGGER " <> deleteTriggerName <> " AFTER DELETE ON \"" <> tableName <> "\" FOR EACH STATEMENT EXECUTE PROCEDURE " <> functionName <> "();\n"+ <> "EXCEPTION\n"+ <> " WHEN SQLSTATE 'XX000' THEN null; -- 'tuple concurrently updated': another connection installed it first\n"+ <> "END; $$" where functionName = "ar_notify_did_change_" <> tableName insertTriggerName = "ar_did_insert_" <> tableName updateTriggerName = "ar_did_update_" <> tableName deleteTriggerName = "ar_did_delete_" <> tableName++autoRefreshStateVaultKey :: Vault.Key AutoRefreshState+autoRefreshStateVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE autoRefreshStateVaultKey #-}
@@ -6,19 +6,20 @@ module IHP.AutoRefresh.Types where import IHP.Prelude-import IHP.Controller.RequestContext+import Wai.Request.Params.Middleware (Respond) import Control.Concurrent.MVar (MVar) import qualified IHP.PGListener as PGListener+import Network.Wai (Request) -data AutoRefreshState = AutoRefreshDisabled | AutoRefreshEnabled { sessionId :: !UUID }+data AutoRefreshState = AutoRefreshEnabled { sessionId :: !UUID } data AutoRefreshSession = AutoRefreshSession { id :: !UUID- -- | A callback to rerun an action within a given request context- , renderView :: !(RequestContext -> IO ())+ -- | A callback to rerun an action within the given request and respond+ , renderView :: !(Request -> Respond -> IO ()) -- | MVar that is filled whenever some table changed , event :: !(MVar ()) -- | All tables this auto refresh session watches- , tables :: !(Set ByteString)+ , tables :: !(Set Text) -- | The last rendered html of this action. Initially this is the result of the initial page rendering , lastResponse :: !LByteString -- | Keep track of the last ping to this session to close it after too much time has passed without anything happening@@ -28,7 +29,7 @@ data AutoRefreshServer = AutoRefreshServer { subscriptions :: [PGListener.Subscription] , sessions :: ![AutoRefreshSession]- , subscribedTables :: !(Set ByteString)+ , subscribedTables :: !(Set Text) , pgListener :: PGListener.PGListener }
@@ -5,8 +5,12 @@ import IHP.HSX.QQ (hsx) import qualified Text.Blaze.Html5 as Html5 import IHP.Controller.Context+import IHP.AutoRefresh (autoRefreshStateVaultKey)+import qualified Data.Vault.Lazy as Vault+import Network.Wai (vault) autoRefreshMeta :: (?context :: ControllerContext) => Html5.Html-autoRefreshMeta = case fromFrozenContext @AutoRefreshState of- AutoRefreshDisabled -> mempty- AutoRefreshEnabled { sessionId } -> [hsx|<meta property="ihp-auto-refresh-id" content={tshow sessionId}/>|]+autoRefreshMeta =+ case Vault.lookup autoRefreshStateVaultKey ?context.request.vault of+ Just (AutoRefreshEnabled { sessionId }) -> [hsx|<meta property="ihp-auto-refresh-id" content={tshow sessionId}/>|]+ _ -> mempty
@@ -1,19 +1,19 @@ module IHP.Breadcrumb.Types where -import IHP.Prelude+import Prelude (Show(..), Maybe, (<>), ($))+import Data.Text (Text) import Text.Blaze.Html (Html) import Text.Blaze.Html.Renderer.String (renderHtml)-import ClassyPrelude data BreadcrumbItem = BreadcrumbItem- { breadcrumbLabel :: Html -- ^ The label of a single breadcrumbs item. May be HTML, thus use SVG or font icon.+ { breadcrumbLabel :: Html -- ^ The label of a single breadcrumbs items. May be HTML, thus use SVG or font icon. , url :: Maybe Text -- ^ The URL of the item. If Nothing, it will only show the label. } -- Html doesn't have a Show instance, so we define it manually and use 'renderHtml' for that. instance Show BreadcrumbItem where- show breadcrumbItem = [plain|{ breadcrumbLabel = "#{breadcrumbLabel}", url = #{url} }|]+ show breadcrumbItem = "{ breadcrumbLabel = \"" <> renderedLabel <> "\", url = " <> renderedUrl <> " }" where- breadcrumbLabel = renderHtml $ breadcrumbItem.breadcrumbLabel- url = ClassyPrelude.show $ breadcrumbItem.url+ renderedLabel = renderHtml $ breadcrumbLabel breadcrumbItem+ renderedUrl = show $ url breadcrumbItem
@@ -12,12 +12,11 @@ import IHP.ControllerSupport import Text.Blaze.Html (Html)- import IHP.View.Types (BreadcrumbsView(..), styledBreadcrumb, styledBreadcrumbItem) import IHP.ViewSupport (theCSSFramework) import IHP.ControllerPrelude -renderBreadcrumb :: (?context :: ControllerContext) => [BreadcrumbItem] -> Html+renderBreadcrumb :: (?request :: Request) => [BreadcrumbItem] -> Html renderBreadcrumb breadcrumbItems = styledBreadcrumb theCSSFramework theCSSFramework breadcrumbItems breadcrumbsView where breadcrumbsView = BreadcrumbsView
@@ -7,8 +7,9 @@ ) where -import IHP.Prelude hiding (displayException)-import IHP.Controller.RequestContext+import Prelude+import Control.Monad (when, unless)+import Wai.Request.Params.Middleware (Respond) import Network.HTTP.Types (status403) import Network.Wai import Network.HTTP.Types.Header@@ -16,7 +17,6 @@ import qualified Data.ByteString.Lazy as LBS import IHP.HSX.QQ (hsx) import qualified System.Directory as Directory-import IHP.Controller.Context import IHP.Controller.Response (respondAndExit) @@ -31,7 +31,7 @@ -- > renderHtml EditView { .. } -- -- This will throw an error and prevent the view from being rendered when the current user is not the author of the post.-accessDeniedWhen :: (?context :: ControllerContext) => Bool -> IO ()+accessDeniedWhen :: Bool -> IO () accessDeniedWhen condition = when condition renderAccessDenied -- | Stops the action execution with an access denied message (403) when the access condition is False.@@ -45,7 +45,7 @@ -- > renderHtml EditView { .. } -- -- This will throw an error and prevent the view from being rendered when the current user is not the author of the post.-accessDeniedUnless :: (?context :: ControllerContext) => Bool -> IO ()+accessDeniedUnless :: Bool -> IO () accessDeniedUnless condition = unless condition renderAccessDenied -- | Renders a 403 access denied response. If a static/403.html exists, that is rendered instead of the IHP access denied page.@@ -153,7 +153,7 @@ -- -- You can override the default access denied page by creating a new file at @static/403.html@. Then IHP will render that HTML file instead of displaying the default IHP access denied page. ---renderAccessDenied :: (?context :: ControllerContext) => IO ()+renderAccessDenied :: IO () renderAccessDenied = do response <- buildAccessDeniedResponse respondAndExit response
@@ -19,7 +19,7 @@ -- -- > beforeAction = basicAuth ... -- -basicAuth :: (?context :: ControllerContext) => Text -> Text -> Text -> IO ()+basicAuth :: (?request :: Request) => Text -> Text -> Text -> IO () basicAuth uid pw realm = do let mein = Just (cs uid, cs pw) let cred = join $ fmap extractBasicAuth (getHeader "Authorization")
@@ -1,141 +1,69 @@ {-| Module: IHP.Controller.Context Copyright: (c) digitally induced GmbH, 2020++Re-exports from ihp-context and adds IHP-specific HasField instances+for accessing the WAI Request and FrameworkConfig. -}-module IHP.Controller.Context where+module IHP.Controller.Context+ ( ControllerContext(..)+ , newControllerContext+ , freeze+ , unfreeze+ , putContext+ , fromContext+ , maybeFromContext+ , fromFrozenContext+ , maybeFromFrozenContext+ , ActionType(..)+ ) where -import IHP.Prelude+import Prelude+import Data.IORef (newIORef, readIORef)+import GHC.Records (HasField(..))+import Data.Maybe (fromMaybe) import qualified Data.TMap as TypeMap-import qualified Data.Typeable as Typeable-import IHP.Controller.RequestContext-import IHP.FrameworkConfig+import IHP.FrameworkConfig.Types (FrameworkConfig(..)) import IHP.Log.Types import System.IO.Unsafe (unsafePerformIO)---- | A container storing useful data along the request lifecycle, such as the request, the current user, set current view layout, flash messages, ...------ The controller context is usually accessed via the @?context@ variable. It's availble inside the action and the view. Think of it as a key-value-map where the key is the type of the value.------ You can store information inside the context using 'putContext':------ >>> newtype CurrentLayout = CurrentLayout Html--- >>>--- >>> ?context <- newControllerContext--- >>> putContext (CurrentLayout layout)------ Inside an action you can access the values using 'fromContext':------ >>> (CurrentLayout layout) <- fromContext------ You can freeze the context and then access values without being inside an IO context (like inside views which are pure):------ Call 'freeze' inside an IO part:------ >>> ?context <- freeze ?context------ ('freeze' is automatically called by IHP before rendering a view, so usually you don't need to call it manually)------ Then use the frozen context from your pure code like this:------ >>> let (CurrentLayout layout) = fromFrozenContext in ...------ The context is initially created before a action is going to be executed. Its life cycle looks like this:------ - @newControllerContext@: The new controller context is created--- - The 'IHP.ControllerSupport.runActionWithNewContext' fills in a few default values: The current @?application@ and also the Flash Messages to be rendered in the to-be-generated response.--- - @initContext@: The initContext function of the @InitControllerContext WebApplication@ (inside your FrontController.hs) is called. There application-specific context can be provided. Usually this is the current user and the default layout.--- - @beforeAction@: Here the context could also be modified. E.g. the layout could be overriden here for the whole controller.--- - @action ..@: The action itself.--- - Freezing: Before rendering the response, the container is frozen. Frozen means that all previously mutable fields become immutable.--- - View Rendering: The frozen container is now used inside the view and layout to display information such as the current user or flash messages-data ControllerContext = ControllerContext { requestContext :: RequestContext, customFieldsRef :: IORef TypeMap.TMap }- | FrozenControllerContext { requestContext :: RequestContext, customFields :: TypeMap.TMap }--newControllerContext :: (?requestContext :: RequestContext) => IO ControllerContext-newControllerContext = do- customFieldsRef <- newIORef TypeMap.empty- pure ControllerContext { requestContext = ?requestContext, customFieldsRef }-{-# INLINABLE newControllerContext #-}+import Network.Wai (Request)+import IHP.RequestVault (requestFrameworkConfig)+import IHP.ActionType (ActionType(..)) --- | After freezing a container you can access its values from pure non-IO code by using 'fromFronzenContext'------ Calls to 'putContext' will throw an exception after it's frozen.-freeze :: ControllerContext -> IO ControllerContext-freeze ControllerContext { requestContext, customFieldsRef } = FrozenControllerContext requestContext <$> readIORef customFieldsRef-freeze frozen = pure frozen-{-# INLINABLE freeze #-}+-- Re-export from ihp-context, but we shadow newControllerContext+import IHP.ControllerContext (ControllerContext(..), freeze, unfreeze, putContext, fromContext, maybeFromContext, fromFrozenContext, maybeFromFrozenContext) --- | Returns a unfrozen version of the controller context that can be modified again+-- | Creates a new controller context with the WAI Request stored in the TMap ----- This is used together with 'freeze' by e.g. AutoRefresh to make a immutable copy of the current controller context state-unfreeze :: ControllerContext -> IO ControllerContext-unfreeze FrozenControllerContext { requestContext, customFields } = do- customFieldsRef <- newIORef customFields- pure ControllerContext { .. }-unfreeze ControllerContext {} = error "Cannot call unfreeze on a controller context that is not frozen"-{-# INLINABLE unfreeze #-}-+-- This version stores the Request in the TMap so it can be retrieved+-- via the HasField instance.+newControllerContext :: (?request :: Request) => IO ControllerContext+newControllerContext = do+ customFieldsRef <- newIORef (TypeMap.insert ?request TypeMap.empty)+ pure ControllerContext { customFieldsRef }+{-# INLINE newControllerContext #-} --- | Returns a value from the current controller context------ Throws an exception if the there is no value with the type inside the context------ __Example:__ Read the current user from the context+-- | Access request from the TMap ----- >>> user <- fromContext @User-fromContext :: forall value. (?context :: ControllerContext, Typeable value) => IO value-fromContext = maybeFromContext @value >>= \case- Just value -> pure value- Nothing -> do- let ControllerContext { customFieldsRef } = ?context+-- This allows @controllerContext.request@ to work by retrieving+-- the WAI Request stored in the TMap.+instance HasField "request" ControllerContext Request where+ getField (FrozenControllerContext { customFields }) =+ case TypeMap.lookup @Request customFields of+ Just req -> req+ Nothing -> error "request: Request not found in controller context. Did you forget to call newControllerContext?"+ getField (ControllerContext { customFieldsRef }) =+ -- Hacky but necessary - we need to read the IORef in a pure context+ unsafePerformIO $ do customFields <- readIORef customFieldsRef- let notFoundMessage = ("Unable to find " <> (show (Typeable.typeRep (Typeable.Proxy @value))) <> " in controller context: " <> show customFields)-- error notFoundMessage-{-# INLINABLE fromContext #-}---- | Returns a value from the current controller context. Requires the context to be frozen.------ __Example:__ Read the current user from the context------ >>> let user = fromFrozenContext @User-fromFrozenContext :: forall value. (?context :: ControllerContext, Typeable value) => value-fromFrozenContext = case maybeFromFrozenContext @value of- Just value -> value- Nothing -> do- let FrozenControllerContext { customFields } = ?context- let notFoundMessage = ("Unable to find " <> (show (Typeable.typeRep (Typeable.Proxy @value))) <> " in controller context: " <> show customFields)-- error notFoundMessage-{-# INLINABLE fromFrozenContext #-}--maybeFromContext :: forall value. (?context :: ControllerContext, Typeable value) => IO (Maybe value)-maybeFromContext = do- frozen <- freeze ?context- let ?context = frozen- pure (maybeFromFrozenContext @value)-{-# INLINABLE maybeFromContext #-}--maybeFromFrozenContext :: forall value. (?context :: ControllerContext, Typeable value) => Maybe value-maybeFromFrozenContext = case ?context of- FrozenControllerContext { customFields } -> TypeMap.lookup @value customFields- ControllerContext {} -> error ("maybeFromFrozenContext called on a non frozen context while trying to access " <> (show (Typeable.typeRep (Typeable.Proxy @value))))-{-# INLINABLE maybeFromFrozenContext #-}---- | Puts a value into the context------ Throws an exception if the context is already frozen.-putContext :: forall value. (?context :: ControllerContext, Typeable value) => value -> IO ()-putContext value = do- let ControllerContext { customFieldsRef } = ?context- modifyIORef customFieldsRef (TypeMap.insert value)- pure ()-{-# INLINABLE putContext #-}--newtype ActionType = ActionType Typeable.TypeRep+ case TypeMap.lookup @Request customFields of+ Just req -> pure req+ Nothing -> error "request: Request not found in controller context. Did you forget to call newControllerContext?"+ {-# INLINABLE getField #-} +-- | Access frameworkConfig via the request vault instance HasField "frameworkConfig" ControllerContext FrameworkConfig where- getField controllerContext = controllerContext.requestContext.frameworkConfig+ getField controllerContext = requestFrameworkConfig controllerContext.request {-# INLINABLE getField #-} -- The following hack is bad, but allows us to override the logger using 'putContext'
@@ -21,7 +21,7 @@ -- > , setCookieValue = "exampleValue" -- > } -- -setCookie :: (?context :: ControllerContext) => SetCookie -> IO ()+setCookie :: (?request :: Request) => SetCookie -> IO () setCookie cookie = setHeader ("Set-Cookie", cookieString) where cookieString = cookie@@ -34,11 +34,11 @@ -- > getCookie "fbc" -- Just "1234" ---getCookie :: (?context :: ControllerContext) => Text -> Maybe Text+getCookie :: (?request :: Request) => Text -> Maybe Text getCookie name = lookup name allCookies -- | Returns all cookies sent with the current request-allCookies :: (?context :: ControllerContext) => [(Text, Text)]+allCookies :: (?request :: Request) => [(Text, Text)] allCookies = maybe [] parseCookiesText $ lookup "Cookie" request.requestHeaders
@@ -15,10 +15,11 @@ import IHP.Prelude import Network.Wai.Parse (FileInfo, fileContent)+import Network.Wai (Request) import qualified IHP.ModelSupport as ModelSupport import qualified Data.ByteString.Lazy as LBS-import IHP.Controller.RequestContext-import IHP.Controller.Context+import Wai.Request.Params.Middleware (RequestBody (..))+import IHP.Controller.Context () import qualified System.Process as Process -- | Returns a file upload from the request as a ByteString.@@ -51,9 +52,9 @@ -- -- See 'IHP.FileStorage.ControllerFunctions.storeFile' to upload the file to S3 or similar cloud storages. ---fileOrNothing :: (?context :: ControllerContext) => ByteString -> Maybe (FileInfo LBS.ByteString)+fileOrNothing :: (?request :: Request) => ByteString -> Maybe (FileInfo LBS.ByteString) fileOrNothing !name =- case ?context.requestContext.requestBody of+ case ?request.parsedBody of FormBody { files } -> -- Search for the file, and confirm it's not an empty one. case lookup name files of@@ -96,9 +97,9 @@ -- > pure () -- > ---filesByName :: (?context :: ControllerContext) => ByteString -> [FileInfo LBS.ByteString]+filesByName :: (?request :: Request) => ByteString -> [FileInfo LBS.ByteString] filesByName !name =- case ?context.requestContext.requestBody of+ case ?request.parsedBody of FormBody { files } -> files |> filter (\(filename, _) -> filename == name) |> map snd@@ -142,7 +143,7 @@ -- -- The uploaded image path is now stored in #pictureUrl. uploadImageWithOptions :: forall (fieldName :: Symbol) record (tableName :: Symbol). (- ?context :: ControllerContext+ ?request :: Request , SetField fieldName record (Maybe Text) , KnownSymbol fieldName , HasField "id" record (ModelSupport.Id (ModelSupport.NormalizeModel record))@@ -192,7 +193,7 @@ -- > redirectTo EditUserAction { .. } -- uploadImageFile :: forall (fieldName :: Symbol) record (tableName :: Symbol). (- ?context :: ControllerContext+ ?request :: Request , SetField fieldName record (Maybe Text) , KnownSymbol fieldName , HasField "id" record (ModelSupport.Id (ModelSupport.NormalizeModel record))@@ -218,7 +219,7 @@ -- | Saves an uploaded png file. No validation or transformation applied. -- See 'uploadImageFile' for details. uploadPng ::- ( ?context :: ControllerContext+ ( ?request :: Request , SetField fieldName record (Maybe Text) , HasField "id" record (ModelSupport.Id' (GetTableName (ModelSupport.GetModelByTableName (GetTableName record)))) , Show (ModelSupport.PrimaryKey (GetTableName (ModelSupport.GetModelByTableName (GetTableName record))))@@ -230,7 +231,7 @@ -- | Saves an uploaded svg file. No validation or transformation applied. -- See 'uploadImageFile' for details. uploadSVG ::- ( ?context :: ControllerContext+ ( ?request :: Request , SetField fieldName record (Maybe Text) , HasField "id" record (ModelSupport.Id' (GetTableName (ModelSupport.GetModelByTableName (GetTableName record)))) , Show (ModelSupport.PrimaryKey (GetTableName (ModelSupport.GetModelByTableName (GetTableName record))))
@@ -6,20 +6,54 @@ ( setLayout , getLayout , ViewLayout (..)+, viewLayoutVaultKey+, viewLayoutMiddleware ) where -import IHP.Prelude+import Prelude import IHP.ViewSupport import IHP.Controller.Context+import Network.Wai (Request, Middleware, vault)+import System.IO.Unsafe (unsafePerformIO)+import qualified Data.Vault.Lazy as Vault+import Data.IORef -newtype ViewLayout = ViewLayout ((?context :: ControllerContext) => Layout)+-- | Wrapper for a layout function that will be applied to views+newtype ViewLayout = ViewLayout ((?context :: ControllerContext, ?request :: Request) => Layout) -setLayout :: (?context :: ControllerContext) => ((?context :: ControllerContext) => Layout) -> IO ()-setLayout layout = do- putContext (ViewLayout layout)+-- | Vault key for storing the mutable layout IORef in each request+viewLayoutVaultKey :: Vault.Key (IORef ViewLayout)+viewLayoutVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE viewLayoutVaultKey #-}++-- | Middleware that initializes the layout IORef with the identity layout.+-- This must be installed in the middleware stack for setLayout/getLayout to work.+{-# INLINE viewLayoutMiddleware #-}+viewLayoutMiddleware :: Middleware+viewLayoutMiddleware app request respond = do+ ref <- newIORef (ViewLayout id)+ let request' = request { vault = Vault.insert viewLayoutVaultKey ref (vault request) }+ app request' respond++-- | Set the layout to be used when rendering views.+--+-- Example:+--+-- > instance InitControllerContext WebApplication where+-- > initContext = do+-- > setLayout defaultLayout+--+setLayout :: (?context :: ControllerContext, ?request :: Request) => ((?context :: ControllerContext, ?request :: Request) => Layout) -> IO ()+setLayout layout =+ case Vault.lookup viewLayoutVaultKey (vault ?request) of+ Just ref -> writeIORef ref (ViewLayout layout)+ Nothing -> error "viewLayoutMiddleware not installed. Add it to your middleware stack in Server.hs" {-# INLINE setLayout #-} -getLayout :: (?context :: ControllerContext) => IO Layout-getLayout = do- (ViewLayout layout) <- fromContext- pure layout+-- | Get the current layout. Returns the identity layout if none was set.+{-# INLINE getLayout #-}+getLayout :: (?request :: Request) => IO ViewLayout+getLayout =+ case Vault.lookup viewLayoutVaultKey (vault ?request) of+ Just ref -> readIORef ref+ Nothing -> error "viewLayoutMiddleware not installed. Add it to your middleware stack in Server.hs"
@@ -7,8 +7,9 @@ ) where -import IHP.Prelude hiding (displayException)-import IHP.Controller.RequestContext+import Prelude+import Control.Monad (when, unless)+import Wai.Request.Params.Middleware (Respond) import Network.HTTP.Types (status404) import Network.Wai import Network.HTTP.Types.Header@@ -16,7 +17,6 @@ import qualified Data.ByteString.Lazy as LBS import IHP.HSX.QQ (hsx) import qualified System.Directory as Directory-import IHP.Controller.Context import IHP.Controller.Response (respondAndExit) @@ -31,7 +31,7 @@ -- > renderHtml EditView { .. } -- -- This will throw an error and prevent the view from being rendered when the current user is not the author of the post.-notFoundWhen :: (?context :: ControllerContext) => Bool -> IO ()+notFoundWhen :: Bool -> IO () notFoundWhen condition = when condition renderNotFound -- | Stops the action execution with a not found message (404) when the access condition is False.@@ -45,7 +45,7 @@ -- > renderHtml EditView { .. } -- -- This will throw an error and prevent the view from being rendered when the current user is not the author of the post.-notFoundUnless :: (?context :: ControllerContext) => Bool -> IO ()+notFoundUnless :: Bool -> IO () notFoundUnless condition = unless condition renderNotFound @@ -153,7 +153,7 @@ -- -- You can override the default access denied page by creating a new file at @static/403.html@. Then IHP will render that HTML file instead of displaying the default IHP access denied page. ---renderNotFound :: (?context :: ControllerContext) => IO ()+renderNotFound :: IO () renderNotFound = do response <- buildNotFoundResponse respondAndExit response
@@ -4,30 +4,56 @@ Module: IHP.Controller.Param Description: Accessing query parameters and the request body Copyright: (c) digitally induced GmbH, 2020++This module provides IHP-style parameter parsing using implicit parameters.+It wraps the generic functionality from "Wai.Request.Params" with IHP's+implicit @?request@ convention and adds IHP-specific 'ParamReader' instances. -}-module IHP.Controller.Param where+module IHP.Controller.Param+( -- * Reading parameters (implicit param versions)+ param+, paramOrNothing+, paramOrDefault+, paramOrError+, paramList+, paramListOrNothing+, hasParam+, queryOrBodyParam+, allParams+ -- * Specialized param functions+, paramText+, paramInt+, paramBool+, paramUUID+ -- * ParamReader typeclass (re-exported from Wai.Request.Params)+, ParamReader (..)+ -- * Exceptions (re-exported from Wai.Request.Params)+, ParamException (..)+ -- * Helper functions for custom ParamReader instances+, enumParamReader+, enumParamReaderJSON+ -- * Form filling+, FillParams (..)+, ifValid+, ifNew+ -- * Utilities+, emptyValueToNothing+) where import IHP.Prelude-import qualified Data.Either as Either-import IHP.Controller.RequestContext-import qualified Network.Wai as Wai-import qualified Data.UUID as UUID+import Network.Wai (Request) import qualified IHP.ModelSupport as ModelSupport-import qualified Data.ByteString.Char8 as Char8 import IHP.ValidationSupport-import GHC.TypeLits import qualified Data.Attoparsec.ByteString.Char8 as Attoparsec-import qualified GHC.Float as Float-import qualified Control.Exception as Exception-import IHP.Controller.Context import qualified Data.Aeson as Aeson-import qualified Data.Aeson.KeyMap as Aeson-import qualified Data.Aeson.Key as Aeson-import qualified Data.Scientific as Scientific-import qualified Data.Vector as Vector+import IHP.RequestVault () import qualified Control.DeepSeq as DeepSeq import Text.Read (readMaybe) +-- Import the generic implementation+import qualified Wai.Request.Params as Params+import Wai.Request.Params (ParamReader(..), ParamException(..), enumParamReaderJSON)+ -- | Returns a query or body parameter from the current request. The raw string -- value is parsed before returning it. So the return value type depends on what -- you expect (e.g. can be Int, Text, UUID, Bool, some custom type).@@ -86,10 +112,8 @@ -- 'ParamNotFoundException' to be thrown with: -- -- > param: Parameter 'firstname' not found-param :: (?context :: ControllerContext) => (ParamReader valueType) => ByteString -> valueType-param !name = case paramOrError name of- Left exception -> Exception.throw exception- Right value -> value+param :: (?request :: Request) => (ParamReader valueType) => ByteString -> valueType+param !name = Params.param ?request.parsedBody ?request name {-# INLINABLE param #-} -- | Similiar to 'param' but works with multiple params. Useful when working with checkboxes.@@ -111,14 +135,8 @@ -- When a value cannot be parsed, this function will fail similiar to 'param'. -- -- Related: https://stackoverflow.com/questions/63875081/how-can-i-pass-list-params-in-ihp-forms/63879113-paramList :: forall valueType. (?context :: ControllerContext, DeepSeq.NFData valueType, ParamReader valueType) => ByteString -> [valueType]-paramList name =- allParams- |> filter (\(paramName, paramValue) -> paramName == name)- |> mapMaybe (\(paramName, paramValue) -> paramValue)- |> map (readParameter @valueType)- |> map (Either.fromRight (error (paramParserErrorMessage name)))- |> DeepSeq.force+paramList :: forall valueType. (?request :: Request, DeepSeq.NFData valueType, ParamReader valueType) => ByteString -> [valueType]+paramList name = Params.paramList ?request.parsedBody ?request name {-# INLINABLE paramList #-} -- | Similiar to 'paramOrNothing' but works with multiple params. This is useful when submitting multiple@@ -139,53 +157,32 @@ -- [] -- ---paramListOrNothing :: forall valueType. (?context :: ControllerContext, DeepSeq.NFData valueType, ParamReader valueType) => ByteString -> [Maybe valueType]-paramListOrNothing name =- allParams- |> filter (\(paramName, paramValue) -> paramName == name)- |> mapMaybe (\(paramName, paramValue) -> paramValue)- |> map (\paramValue -> if paramValue == "" then Left "Empty ByteString" else readParameter @valueType paramValue)- |> map (\value -> case value of- Left _ -> Nothing- Right val -> Just val- )- |> DeepSeq.force+paramListOrNothing :: forall valueType. (?request :: Request, DeepSeq.NFData valueType, ParamReader valueType) => ByteString -> [Maybe valueType]+paramListOrNothing name = Params.paramListOrNothing ?request.parsedBody ?request name {-# INLINABLE paramListOrNothing #-} -paramParserErrorMessage name = "param: Parameter '" <> cs name <> "' is invalid"---- | Thrown when a parameter is missing when calling 'param "myParam"' or related functions-data ParamException- = ParamNotFoundException { name :: ByteString }- | ParamCouldNotBeParsedException { name :: ByteString, parserError :: ByteString }- deriving (Show, Eq)--instance Exception ParamException where- displayException (ParamNotFoundException { name }) = "param: Parameter '" <> cs name <> "' not found"- displayException (ParamCouldNotBeParsedException { name, parserError }) = "param: Parameter '" <> cs name <> "' could not be parsed, " <> cs parserError- -- | Specialized version of param for 'Text'. -- -- This way you don't need to know about the type application syntax.-paramText :: (?context :: ControllerContext) => ByteString -> Text+paramText :: (?request :: Request) => ByteString -> Text paramText = param @Text -- | Specialized version of param for 'Int'. -- -- This way you don't need to know about the type application syntax.-paramInt :: (?context :: ControllerContext) => ByteString -> Int+paramInt :: (?request :: Request) => ByteString -> Int paramInt = param @Int -- | Specialized version of param for 'Bool'. -- -- This way you don't need to know about the type application syntax.-paramBool :: (?context :: ControllerContext) => ByteString -> Bool+paramBool :: (?request :: Request) => ByteString -> Bool paramBool = param @Bool -- | Specialized version of param for 'UUID'. -- -- This way you don't need to know about the type application syntax.-paramUUID :: (?context :: ControllerContext) => ByteString -> UUID+paramUUID :: (?request :: Request) => ByteString -> UUID paramUUID = param @UUID -- | Returns @True@ when a parameter is given in the request via the query or request body.@@ -202,8 +199,8 @@ -- > else renderPlain "Please provide your firstname" -- -- This will render @Please provide your firstname@ because @hasParam "firstname"@ returns @False@-hasParam :: (?context :: ControllerContext) => ByteString -> Bool-hasParam = isJust . queryOrBodyParam+hasParam :: (?request :: Request) => ByteString -> Bool+hasParam = Params.hasParam ?request.parsedBody ?request {-# INLINABLE hasParam #-} -- | Like 'param', but returns a default value when the parameter is missing instead of throwing@@ -219,8 +216,8 @@ -- > let page :: Int = paramOrDefault 0 "page" -- -- When calling @GET /Users?page=1@ the variable @page@ will be set to @1@.-paramOrDefault :: (?context :: ControllerContext) => ParamReader a => a -> ByteString -> a-paramOrDefault !defaultValue = fromMaybe defaultValue . paramOrNothing+paramOrDefault :: (?request :: Request) => ParamReader a => a -> ByteString -> a+paramOrDefault !defaultValue name = Params.paramOrDefault ?request.parsedBody ?request defaultValue name {-# INLINABLE paramOrDefault #-} -- | Like 'param', but returns @Nothing@ the parameter is missing instead of throwing@@ -236,146 +233,60 @@ -- > let page :: Maybe Int = paramOrNothing "page" -- -- When calling @GET /Users?page=1@ the variable @page@ will be set to @Just 1@.-paramOrNothing :: forall paramType. (?context :: ControllerContext) => ParamReader (Maybe paramType) => ByteString -> Maybe paramType-paramOrNothing !name =- case paramOrError name of- Left ParamNotFoundException {} -> Nothing- Left otherException -> Exception.throw otherException- Right value -> value+paramOrNothing :: forall paramType. (?request :: Request) => ParamReader (Maybe paramType) => ByteString -> Maybe paramType+paramOrNothing !name = Params.paramOrNothing ?request.parsedBody ?request name {-# INLINABLE paramOrNothing #-} -- | Like 'param', but returns @Left "Some error message"@ if the parameter is missing or invalid-paramOrError :: forall paramType. (?context :: ControllerContext) => ParamReader paramType => ByteString -> Either ParamException paramType-paramOrError !name =- let- RequestContext { requestBody } = ?context.requestContext- in case requestBody of- FormBody {} -> case queryOrBodyParam name of- Just value -> case readParameter @paramType value of- Left parserError -> Left ParamCouldNotBeParsedException { name, parserError }- Right value -> Right value- Nothing -> Left ParamNotFoundException { name }- JSONBody { jsonPayload } -> case jsonPayload of- (Just (Aeson.Object hashMap)) -> case Aeson.lookup (Aeson.fromText $ cs name) hashMap of- Just value -> case readParameterJSON @paramType value of- Left parserError -> Left ParamCouldNotBeParsedException { name, parserError }- Right value -> Right value- Nothing -> Left ParamNotFoundException { name }- _ -> Left ParamNotFoundException { name }+paramOrError :: forall paramType. (?request :: Request) => ParamReader paramType => ByteString -> Either ParamException paramType+paramOrError !name = Params.paramOrError ?request.parsedBody ?request name {-# INLINABLE paramOrError #-} -- | Returns a parameter without any parsing. Returns @Nothing@ when the parameter is missing.-queryOrBodyParam :: (?context :: ControllerContext) => ByteString -> Maybe ByteString-queryOrBodyParam !name = join (lookup name allParams)+queryOrBodyParam :: (?request :: Request) => ByteString -> Maybe ByteString+queryOrBodyParam !name = Params.queryOrBodyParam ?request.parsedBody ?request name {-# INLINABLE queryOrBodyParam #-} -- | Returns all params available in the current request-allParams :: (?context :: ControllerContext) => [(ByteString, Maybe ByteString)]-allParams = case requestBody of- FormBody { params, files } -> concat [(map (\(a, b) -> (a, Just b)) params), (Wai.queryString request)]- JSONBody { jsonPayload } -> error "allParams: Not supported for JSON requests"- where- RequestContext { request, requestBody } = ?context.requestContext---- | Input parser for 'param'.------ Parses the input bytestring. Returns @Left "some error"@ when there is an error parsing the value.--- Returns @Right value@ when the parsing succeeded.-class ParamReader a where- -- | The error messages here should be human-readable, as they're visible e.g. in forms- readParameter :: ByteString -> Either ByteString a- -- | The error messages here are directed at other developers, so they can be a bit more technical than 'readParameter' errors- readParameterJSON :: Aeson.Value -> Either ByteString a- readParameterJSON = enumParamReaderJSON--instance ParamReader ByteString where- {-# INLINABLE readParameter #-}- readParameter byteString = pure byteString-- readParameterJSON (Aeson.String bytestring) = Right (cs bytestring)- readParameterJSON _ = Left "Expected String"--instance ParamReader Int where- {-# INLINABLE readParameter #-}- readParameter byteString =- case Attoparsec.parseOnly ((Attoparsec.signed Attoparsec.decimal) <* Attoparsec.endOfInput) byteString of- Right value -> Right value- Left error -> Left "has to be an integer"-- readParameterJSON (Aeson.Number number) =- case Scientific.floatingOrInteger number of- Left float -> Left "Expected Int"- Right int -> Right int- readParameterJSON _ = Left "Expected Int"--instance ParamReader Integer where- {-# INLINABLE readParameter #-}- readParameter byteString =- case Attoparsec.parseOnly ((Attoparsec.signed Attoparsec.decimal) <* Attoparsec.endOfInput) byteString of- Right value -> Right value- Left error -> Left "has to be an integer"-- readParameterJSON (Aeson.Number number) =- case Scientific.floatingOrInteger number of- Left float -> Left "Expected Integer"- Right integer -> Right integer- readParameterJSON _ = Left "Expected Integer"--instance ParamReader Double where- {-# INLINABLE readParameter #-}- readParameter byteString =- case Attoparsec.parseOnly (Attoparsec.double <* Attoparsec.endOfInput) byteString of- Right value -> Right value- Left error -> Left "has to be a number with decimals"-- readParameterJSON (Aeson.Number number) =- case Scientific.floatingOrInteger number of- Left double -> Right double- Right integer -> Right (fromIntegral integer)- readParameterJSON _ = Left "Expected Double"--instance ParamReader Scientific.Scientific where- {-# INLINABLE readParameter #-}- readParameter byteString =- case Attoparsec.parseOnly (Attoparsec.scientific <* Attoparsec.endOfInput) byteString of- Right value -> Right value- Left error -> Left "has to be a number with decimals"-- readParameterJSON (Aeson.Number number) = Right number- readParameterJSON _ = Left "Expected Scientific"--instance ParamReader Float where- {-# INLINABLE readParameter #-}- readParameter byteString =- case Attoparsec.parseOnly (Attoparsec.double <* Attoparsec.endOfInput) byteString of- Right value -> Right (Float.double2Float value)- Left error -> Left "has to be a number with decimals"+allParams :: (?request :: Request) => [(ByteString, Maybe ByteString)]+allParams = Params.allParams ?request.parsedBody ?request - readParameterJSON (Aeson.Number number) =- case Scientific.floatingOrInteger number of- Left double -> Right double- Right integer -> Right (fromIntegral integer)- readParameterJSON _ = Left "Expected Float"+-- IHP-specific ParamReader instances instance ParamReader ModelSupport.Point where {-# INLINABLE readParameter #-} readParameter byteString =- case Attoparsec.parseOnly (do x <- Attoparsec.double; Attoparsec.char ','; y <- Attoparsec.double; Attoparsec.endOfInput; pure ModelSupport.Point { x, y }) byteString of+ case Attoparsec.parseOnly (do x <- Attoparsec.double; Attoparsec.char ','; y <- Attoparsec.double; Attoparsec.endOfInput; pure (ModelSupport.fromCoordinates x y)) byteString of Right value -> Right value Left error -> Left "has to be two numbers with a comma, e.g. '1,2'" readParameterJSON (Aeson.String string) = let byteString :: ByteString = cs string in readParameter byteString readParameterJSON _ = Left "Expected Point" -instance ParamReader ModelSupport.PGInterval where+instance ParamReader ModelSupport.Interval where {-# INLINABLE readParameter #-}- readParameter byteString = pure (ModelSupport.PGInterval byteString)+ readParameter byteString = case readMaybe (cs byteString) of+ Just interval -> Right interval+ Nothing -> Left "Invalid interval" - readParameterJSON (Aeson.String bytestring) = Right (ModelSupport.PGInterval (cs bytestring))+ readParameterJSON (Aeson.String string) = case readMaybe (cs string) of+ Just interval -> Right interval+ Nothing -> Left "Invalid interval" readParameterJSON _ = Left "Expected String" +instance ParamReader ModelSupport.Inet where+ {-# INLINABLE readParameter #-}+ readParameter byteString = case readMaybe (cs byteString) of+ Just inet -> Right inet+ Nothing -> Left "Invalid IP address" + readParameterJSON (Aeson.String string) = case readMaybe (cs string) of+ Just inet -> Right inet+ Nothing -> Left "Invalid IP address"+ readParameterJSON _ = Left "Expected String" ++ instance ParamReader ModelSupport.Polygon where {-# INLINABLE readParameter #-} readParameter byteString =@@ -386,173 +297,26 @@ Attoparsec.char ',' y <- Attoparsec.double Attoparsec.char ')'- pure ModelSupport.Point { .. }+ pure (x, y) parser = do points <- pointParser `Attoparsec.sepBy` (Attoparsec.char ',') Attoparsec.endOfInput- pure ModelSupport.Polygon { .. }+ case ModelSupport.refineFromPointList points of+ Just polygon -> pure polygon+ Nothing -> fail "Polygon must have at least 3 points" in case Attoparsec.parseOnly parser byteString of Right value -> Right value- Left error -> Left "has to be points wrapped in parenthesis, separated with a comma, e.g. '(1,2),(3,4)'"+ Left error -> Left (cs error) readParameterJSON (Aeson.String string) = let byteString :: ByteString = cs string in readParameter byteString readParameterJSON _ = Left "Expected Polygon" -instance ParamReader Text where- {-# INLINABLE readParameter #-}- readParameter byteString = pure (cs byteString)-- readParameterJSON (Aeson.String text) = Right text- readParameterJSON _ = Left "Expected String"---- | Parses comma separated input like @userIds=1,2,3@------ __Example:__------ >>> let userIds :: [Int] = param "userIds"-instance ParamReader value => ParamReader [value] where- {-# INLINABLE readParameter #-}- readParameter byteString =- byteString- |> Char8.split ','- |> map readParameter- |> Either.partitionEithers- |> \case- ([], values) -> Right values- ((first:rest), _) -> Left first-- readParameterJSON (Aeson.Array values) =- values- |> Vector.toList- |> map readParameterJSON- |> Either.partitionEithers- |> \case- ([], values) -> Right values- ((first:rest), _) -> Left first- readParameterJSON _ = Left "Expected Array"---- | Parses a boolean.------ Html form checkboxes usually use @on@ or @off@ for representation. These--- values are supported here.-instance ParamReader Bool where- {-# INLINABLE readParameter #-}- readParameter on | on == cs (ModelSupport.inputValue True) = pure True- readParameter true | toLower (cs true) == "true" = pure True- readParameter _ = pure False-- readParameterJSON (Aeson.Bool bool) = Right bool- readParameterJSON _ = Left "Expected Bool"--instance ParamReader UUID where- {-# INLINABLE readParameter #-}- readParameter byteString =- case UUID.fromASCIIBytes byteString of- Just uuid -> pure uuid- Nothing -> Left "has to be an UUID"-- readParameterJSON (Aeson.String string) =- case UUID.fromText string of- Just uuid -> pure uuid- Nothing -> Left "Invalid UUID"- readParameterJSON _ = Left "Expected String with an UUID"---- | Accepts values such as @2020-11-08T12:03:35Z@ or @2020-11-08@-instance ParamReader UTCTime where- {-# INLINABLE readParameter #-}- readParameter "" = Left "This field cannot be empty"- readParameter byteString =- let- input = (cs byteString)- dateTime = parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" input- date = parseTimeM True defaultTimeLocale "%Y-%-m-%-d" input- in case dateTime of- Nothing -> case date of- Just value -> Right value- Nothing -> Left "has to be a valid date and time, e.g. 2020-11-08T12:03:35Z"- Just value -> Right value-- readParameterJSON (Aeson.String string) = readParameter (cs string)- readParameterJSON _ = Left "Expected String"---- | Accepts values such as @2020-11-08T12:03:35Z@ or @2020-11-08@-instance ParamReader LocalTime where- {-# INLINABLE readParameter #-}- readParameter "" = Left "This field cannot be empty"- readParameter byteString =- let- input = (cs byteString)- dateTime = parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" input- date = parseTimeM True defaultTimeLocale "%Y-%-m-%-d" input- in case dateTime of- Nothing -> case date of- Just value -> Right value- Nothing -> Left "has to be a valid date and time, e.g. 2020-11-08T12:03:35Z"- Just value -> Right value-- readParameterJSON (Aeson.String string) = readParameter (cs string)- readParameterJSON _ = Left "Expected String"---- | Accepts values such as @2020-11-08@-instance ParamReader Day where- {-# INLINABLE readParameter #-}- readParameter "" = Left "This field cannot be empty"- readParameter byteString =- let- input = (cs byteString)- date = parseTimeM True defaultTimeLocale "%Y-%-m-%-d" input- in case date of- Just value -> Right value- Nothing -> Left "has to be a date, e.g. 2020-11-08"-- readParameterJSON (Aeson.String string) = readParameter (cs string)- readParameterJSON _ = Left "Expected String"--instance ParamReader TimeOfDay where- {-# INLINABLE readParameter #-}- readParameter "" = Left "This field cannot be empty"- readParameter byteString =- let- input = (cs byteString)- in case readMaybe input of- Just value -> Right value- Nothing -> Left "has to be time in the format hh:mm:ss"-- readParameterJSON (Aeson.String string) = readParameter (cs string)- readParameterJSON _ = Left "Expected String"- instance {-# OVERLAPS #-} (ParamReader (ModelSupport.PrimaryKey model')) => ParamReader (ModelSupport.Id' model') where {-# INLINABLE readParameter #-} readParameter uuid = ModelSupport.Id <$> readParameter uuid readParameterJSON value = ModelSupport.Id <$> readParameterJSON value -instance ParamReader param => ParamReader (Maybe param) where- {-# INLINABLE readParameter #-}- readParameter param =- case (readParameter param) :: Either ByteString param of- Right value -> Right (Just value)- Left error | param == "" -> Right Nothing- Left error -> Left error-- readParameterJSON value =- case (readParameterJSON value) :: Either ByteString param of- Right value -> Right (Just value)- Left error | value == (Aeson.String "") -> Right Nothing- Left error -> Left error---- | Custom error hint when the 'param' is called with do-notation------ __Example:__------ > action Example = do--- > myParam <- param "hello"------ Now a custom type error will be shown telling the user to use @let myParam = param "hello"@ instead of do-notation.-instance (TypeError ('Text ("Use 'let x = param \"..\"' instead of 'x <- param \"..\"'" :: Symbol))) => ParamReader (IO param) where- readParameter _ = error "Unreachable"- readParameterJSON _ = error "Unreachable"- -- | Can be used as a default implementation for 'readParameter' for enum structures -- -- __Example:__@@ -570,19 +334,6 @@ where string' = cs string --- | Used as a default implementation for 'readParameterJSON'------ __Example:__------ > data Color = Yellow | Red | Blue deriving (Enum)--- >--- > instance ParamReader Color where--- > readParameter = enumParamReader--- > readParameterJSON = enumParamReaderJSON-enumParamReaderJSON :: forall parameter. (ParamReader parameter) => Aeson.Value -> Either ByteString parameter-enumParamReaderJSON (Aeson.String string) = readParameter (cs string)-enumParamReaderJSON otherwise = Left "enumParamReaderJSON: Invalid value, expected a string but got something else"- -- | Provides the 'fill' function for mass-assignment of multiple parameters to a record -- -- Accepts a type-level list of parameter names (type-list syntax is like @\@'["a", "b", "c"]@) and a record. Then each parameter is@@ -606,7 +357,7 @@ -- This code will read the firstname, lastname and email from the request and assign them to the user. class FillParams (params :: [Symbol]) record where fill :: (- ?context :: ControllerContext+ ?request :: Request , HasField "meta" record ModelSupport.MetaBag , SetField "meta" record ModelSupport.MetaBag ) => record -> record@@ -639,7 +390,7 @@ else Left model {-# INLINE ifValid #-} -ifNew :: forall record. (?context :: ControllerContext, ?modelContext :: ModelSupport.ModelContext, HasField "meta" record MetaBag) => (record -> record) -> record -> record+ifNew :: forall record. (?modelContext :: ModelSupport.ModelContext, HasField "meta" record MetaBag) => (record -> record) -> record -> record ifNew thenBlock record = if ModelSupport.isNew record then thenBlock record else record
@@ -7,20 +7,22 @@ ( redirectTo , redirectToPath , redirectToUrl+, redirectToSeeOther+, redirectToPathSeeOther+, redirectToUrlSeeOther , redirectBack , redirectBackWithFallback ) where -import IHP.Prelude+import Prelude+import Data.Text (Text)+import Data.Maybe (fromMaybe)+import Data.String.Conversions (cs, convertString) import qualified Network.Wai.Util-import Network.URI (parseURI)-import IHP.Controller.RequestContext-import IHP.RouterSupport (HasPath (pathTo))-import IHP.FrameworkConfig+import Network.URI (parseURI, uriToString)+import IHP.Router.UrlGenerator (HasPath (pathTo)) import Network.HTTP.Types.Status import qualified Network.Wai.Middleware.Approot as Approot--import IHP.Controller.Context import IHP.ControllerSupport -- | Redirects to an action@@ -30,10 +32,17 @@ -- > redirectTo ShowProjectAction { projectId = project.id } -- -- Use 'redirectToPath' if you want to redirect to a non-action url.-redirectTo :: (?context :: ControllerContext, HasPath action) => action -> IO ()+redirectTo :: (?request :: Request, HasPath action) => action -> IO () redirectTo action = redirectToPath (pathTo action) {-# INLINABLE redirectTo #-} +-- | Redirects to an action using HTTP 303 See Other+--+-- Forces the follow-up request to be a GET (useful after POST/DELETE).+redirectToSeeOther :: (?request :: Request, HasPath action) => action -> IO ()+redirectToSeeOther action = redirectToPathSeeOther (pathTo action)+{-# INLINABLE redirectToSeeOther #-}+ -- TODO: redirectTo user -- | Redirects to a path (given as a string)@@ -43,12 +52,21 @@ -- > redirectToPath "/blog/wp-login.php" -- -- Use 'redirectTo' if you want to redirect to a controller action.-redirectToPath :: (?context :: ControllerContext) => Text -> IO ()+redirectToPath :: (?request :: Request) => Text -> IO () redirectToPath path = redirectToUrl (convertString baseUrl <> path) where- baseUrl = Approot.getApproot ?context.requestContext.request+ baseUrl = Approot.getApproot ?request {-# INLINABLE redirectToPath #-} +-- | Redirects to a path using HTTP 303 See Other+--+-- Forces the follow-up request to be a GET (useful after POST/DELETE).+redirectToPathSeeOther :: (?request :: Request) => Text -> IO ()+redirectToPathSeeOther path = redirectToUrlSeeOther (convertString baseUrl <> path)+ where+ baseUrl = Approot.getApproot ?request+{-# INLINABLE redirectToPathSeeOther #-}+ -- | Redirects to a url (given as a string) -- -- __Example:__@@ -56,9 +74,8 @@ -- > redirectToUrl "https://example.com/hello-world.html" -- -- Use 'redirectToPath' if you want to redirect to a relative path like @/hello-world.html@-redirectToUrl :: (?context :: ControllerContext) => Text -> IO ()+redirectToUrl :: Text -> IO () redirectToUrl url = do- let RequestContext { respond } = ?context.requestContext let !parsedUrl = fromMaybe (error ("redirectToPath: Unable to parse url: " <> show url)) (parseURI (cs url))@@ -68,6 +85,19 @@ respondAndExit redirectResponse {-# INLINABLE redirectToUrl #-} +-- | Redirects to a url using HTTP 303 See Other+--+-- Forces the follow-up request to be a GET (useful after POST/DELETE).+redirectToUrlSeeOther :: Text -> IO ()+redirectToUrlSeeOther url = do+ let !parsedUrl = fromMaybe+ (error ("redirectToUrlSeeOther: Unable to parse url: " <> show url))+ (parseURI (cs url))+ let !redirectResponse = fromMaybe+ (error "redirectToUrlSeeOther: Unable to construct redirect response")+ (Network.Wai.Util.redirect status303 [] parsedUrl)+ respondAndExit redirectResponse+{-# INLINABLE redirectToUrlSeeOther #-} -- | Redirects back to the last page --@@ -86,7 +116,7 @@ -- > -- > redirectBack ---redirectBack :: (?context :: ControllerContext) => IO ()+redirectBack :: (?request :: Request) => IO () redirectBack = redirectBackWithFallback "/" {-# INLINABLE redirectBack #-} @@ -104,13 +134,13 @@ -- > -- > redirectBackWithFallback (pathTo ShowPostAction { postId = post.id }) ---redirectBackWithFallback :: (?context :: ControllerContext) => Text -> IO ()+redirectBackWithFallback :: (?request :: Request) => Text -> IO () redirectBackWithFallback fallbackPathOrUrl = do case getHeader "Referer" of Just referer -> case parseURI (cs referer) of- Just uri -> redirectToUrl (tshow uri) -- Referer Is URL "https://google.com/..."+ Just uri -> redirectToUrl (cs (uriToString id uri "")) -- Referer Is URL "https://google.com/..." Nothing -> redirectToPath (cs referer) -- Referer Is Path "/../" Nothing -> case parseURI (cs fallbackPathOrUrl) of- Just uri -> redirectToUrl (tshow uri) -- Fallback Is URL "https://google.com/..."+ Just uri -> redirectToUrl (cs (uriToString id uri "")) -- Fallback Is URL "https://google.com/..." Nothing -> redirectToPath fallbackPathOrUrl -- Fallback Is Path "/../" {-# INLINABLE redirectBackWithFallback #-}
@@ -1,8 +1,7 @@ {-# LANGUAGE BangPatterns #-} module IHP.Controller.Render where import ClassyPrelude-import Network.Wai (responseLBS, responseBuilder, responseFile)-import qualified Network.Wai+import Network.Wai import Network.HTTP.Types (Status, status200, status406) import Network.HTTP.Types.Header import qualified Data.ByteString.Lazy@@ -10,76 +9,67 @@ import qualified Data.Aeson import IHP.ControllerSupport import qualified Network.HTTP.Media as Accept-import qualified Data.List as List + import qualified Text.Blaze.Html.Renderer.Utf8 as Blaze import Text.Blaze.Html (Html) import qualified IHP.Controller.Context as Context import IHP.Controller.Layout-import qualified IHP.FrameworkConfig as FrameworkConfig-import qualified Data.ByteString.Builder as ByteString-import IHP.FlashMessages.ControllerFunctions (initFlashMessages)--renderPlain :: (?context :: ControllerContext) => LByteString -> IO ()-renderPlain text = respondAndExit $ responseLBS status200 [(hContentType, "text/plain")] text-{-# INLINABLE renderPlain #-}+import IHP.FlashMessages (consumeFlashMessagesMiddleware) -respondHtml :: (?context :: ControllerContext) => Html -> IO ()-respondHtml html =- -- The seq is required to force evaluation of `evaluatedBuilder` before returning the IO action. See below for details- evaluatedBuilder `seq` (respondAndExit $ responseBuilder status200 [(hContentType, "text/html; charset=utf-8"), (hConnection, "keep-alive")] evaluatedBuilder)- where- builder = Blaze.renderHtmlBuilder html- builderAsByteString = ByteString.toLazyByteString builder+renderPlain :: (?request :: Request) => LByteString -> IO ()+renderPlain text = respondAndExitWithHeaders $ responseLBS status200 [(hContentType, "text/plain")] text+{-# INLINE renderPlain #-} - -- We force the full evaluation of the blaze html expressions to catch- -- any runtime errors with the IHP error middleware. Without this full evaluation- -- certain thunks might only cause an error when warp is building the response string.- -- But then it's already too late to catch the exception and the user will only get- -- the default warp error message instead of our nice IHP error message design.- evaluatedBuilder = Data.ByteString.Lazy.length builderAsByteString `seq` ByteString.lazyByteString builderAsByteString-{-# INLINABLE respondHtml #-}+respondHtml :: (?request :: Request) => Html -> IO ()+respondHtml html = do+ let !bs = Blaze.renderHtml html+ -- We force the full evaluation of the blaze html to catch any runtime errors+ -- with the IHP error middleware. Without this, certain thunks might only cause+ -- an error when warp is building the response string. But then it's already too+ -- late to catch the exception and the user will only get the default warp error+ -- message instead of our nice IHP error message design.+ _ <- evaluate (Data.ByteString.Lazy.length bs)+ respondAndExitWithHeaders $ responseLBS status200 [(hContentType, "text/html; charset=utf-8"), (hConnection, "keep-alive")] bs+{-# INLINE respondHtml #-} -respondSvg :: (?context :: ControllerContext) => Html -> IO ()-respondSvg html = respondAndExit $ responseBuilder status200 [(hContentType, "image/svg+xml"), (hConnection, "keep-alive")] (Blaze.renderHtmlBuilder html)+respondSvg :: (?request :: Request) => Html -> IO ()+respondSvg html = respondAndExitWithHeaders $ responseBuilder status200 [(hContentType, "image/svg+xml"), (hConnection, "keep-alive")] (Blaze.renderHtmlBuilder html) {-# INLINABLE respondSvg #-} -renderHtml :: forall view. (ViewSupport.View view, ?context :: ControllerContext) => view -> IO Html+renderHtml :: forall view. (ViewSupport.View view, ?context :: ControllerContext, ?request :: Request) => view -> IO Html renderHtml !view = do let ?view = view- initFlashMessages ViewSupport.beforeRender view frozenContext <- Context.freeze ?context let ?context = frozenContext- let layout = case Context.maybeFromFrozenContext @ViewLayout of- Just (ViewLayout layout) -> layout- Nothing -> id+ (ViewLayout layout) <- getLayout - let boundHtml = let ?context = frozenContext in layout (ViewSupport.html ?view)+ let boundHtml = let ?context = frozenContext; in layout (ViewSupport.html ?view) pure boundHtml-{-# INLINABLE renderHtml #-}+{-# INLINE renderHtml #-} -renderFile :: (?context :: ControllerContext) => String -> ByteString -> IO ()-renderFile filePath contentType = respondAndExit $ responseFile status200 [(hContentType, contentType)] filePath Nothing-{-# INLINABLE renderFile #-}+renderFile :: (?request :: Request) => String -> ByteString -> IO ()+renderFile filePath contentType = respondAndExitWithHeaders $ responseFile status200 [(hContentType, contentType)] filePath Nothing+{-# INLINE renderFile #-} -renderJson :: (?context :: ControllerContext) => Data.Aeson.ToJSON json => json -> IO ()+renderJson :: (?request :: Request) => Data.Aeson.ToJSON json => json -> IO () renderJson json = renderJsonWithStatusCode status200 json-{-# INLINABLE renderJson #-}+{-# INLINE renderJson #-} -renderJsonWithStatusCode :: (?context :: ControllerContext) => Data.Aeson.ToJSON json => Status -> json -> IO ()-renderJsonWithStatusCode statusCode json = respondAndExit $ responseLBS statusCode [(hContentType, "application/json")] (Data.Aeson.encode json)-{-# INLINABLE renderJsonWithStatusCode #-}+renderJsonWithStatusCode :: (?request :: Request) => Data.Aeson.ToJSON json => Status -> json -> IO ()+renderJsonWithStatusCode statusCode json = respondAndExitWithHeaders $ responseLBS statusCode [(hContentType, "application/json")] (Data.Aeson.encode json)+{-# INLINE renderJsonWithStatusCode #-} -renderXml :: (?context :: ControllerContext) => LByteString -> IO ()-renderXml xml = respondAndExit $ responseLBS status200 [(hContentType, "application/xml")] xml-{-# INLINABLE renderXml #-}+renderXml :: (?request :: Request) => LByteString -> IO ()+renderXml xml = respondAndExitWithHeaders $ responseLBS status200 [(hContentType, "application/xml")] xml+{-# INLINE renderXml #-} --- | Use 'setHeader' intead-renderJson' :: (?context :: ControllerContext) => ResponseHeaders -> Data.Aeson.ToJSON json => json -> IO ()-renderJson' additionalHeaders json = respondAndExit $ responseLBS status200 ([(hContentType, "application/json")] <> additionalHeaders) (Data.Aeson.encode json)-{-# INLINABLE renderJson' #-}+-- | Use 'setHeader' instead+renderJson' :: (?request :: Request) => ResponseHeaders -> Data.Aeson.ToJSON json => json -> IO ()+renderJson' additionalHeaders json = respondAndExitWithHeaders $ responseLBS status200 ([(hContentType, "application/json")] <> additionalHeaders) (Data.Aeson.encode json)+{-# INLINE renderJson' #-} data PolymorphicRender = PolymorphicRender@@ -98,32 +88,43 @@ -- > } -- -- This will render @Hello World@ for normal browser requests and @true@ when requested via an ajax request-{-# INLINABLE renderPolymorphic #-}-renderPolymorphic :: (?context :: ControllerContext) => PolymorphicRender -> IO ()+{-# INLINE renderPolymorphic #-}+renderPolymorphic :: (?context :: ControllerContext, ?request :: Request) => PolymorphicRender -> IO () renderPolymorphic PolymorphicRender { html, json } = do- let headers = Network.Wai.requestHeaders request- let acceptHeader = snd (fromMaybe (hAccept, "text/html") (List.find (\(headerName, _) -> headerName == hAccept) headers)) :: ByteString- let send406Error = respondAndExit $ responseLBS status406 [] "Could not find any acceptable response format"- let formats = concat [- case html of- Just handler -> [("text/html", handler)]- Nothing -> mempty- ,- case json of- Just handler -> [("application/json", handler)]- Nothing -> mempty- ]- fromMaybe send406Error (Accept.mapAcceptMedia formats acceptHeader)+ let acceptHeader = lookup hAccept (request.requestHeaders)+ case acceptHeader of+ -- Fast path: no Accept header or starts with text/html — dispatch directly+ Nothing | Just handler <- html -> handler+ Just h | "text/html" `isPrefixOf` h, Just handler <- html -> handler+ _ -> do+ let accept = fromMaybe "text/html" acceptHeader+ let send406Error = respondAndExitWithHeaders $ responseLBS status406 [] "Could not find any acceptable response format"+ let formats = concat [+ case html of+ Just handler -> [("text/html", handler)]+ Nothing -> mempty+ ,+ case json of+ Just handler -> [("application/json", handler)]+ Nothing -> mempty+ ]+ fromMaybe send406Error (Accept.mapAcceptMedia formats accept) polymorphicRender :: PolymorphicRender polymorphicRender = PolymorphicRender Nothing Nothing -{-# INLINABLE render #-}-render :: forall view. (ViewSupport.View view, ?context :: ControllerContext) => view -> IO ()+{-# INLINE render #-}+render :: forall view. (ViewSupport.View view, ?context :: ControllerContext, ?request :: Request, ?respond :: Respond) => view -> IO () render !view = do+ let !currentRequest = ?request renderPolymorphic PolymorphicRender- { html = Just $ (renderHtml view) >>= respondHtml+ { html = Just do+ let next request respond = do+ let ?request = request in ((renderHtml view) >>= respondHtml)+ error "unreachable"+ _ <- consumeFlashMessagesMiddleware next currentRequest ?respond+ pure () , json = Just $ renderJson (ViewSupport.json view) }
@@ -1,26 +0,0 @@-module IHP.Controller.RequestContext-( RequestContext (..)-, Respond-, RequestBody (..)-) where--import ClassyPrelude-import qualified Data.ByteString.Lazy as LBS-import Network.Wai (Request, Response, ResponseReceived)-import Network.Wai.Parse (File, Param)-import IHP.FrameworkConfig-import qualified Data.Aeson as Aeson---type Respond = Response -> IO ResponseReceived--data RequestBody- = FormBody { params :: [Param], files :: [File LBS.ByteString] }- | JSONBody { jsonPayload :: Maybe Aeson.Value, rawPayload :: LByteString } -- ^ The jsonPayload is the decoded json request. We keep a copy of the original json request in rawPayload, so that you can e.g. get a HMAC signature from the request--data RequestContext = RequestContext- { request :: Request- , respond :: Respond- , requestBody :: RequestBody- , frameworkConfig :: FrameworkConfig- }
@@ -1,23 +1,33 @@ module IHP.Controller.Response ( respondAndExit+, respondAndExitWithHeaders , addResponseHeaders , addResponseHeadersFromContext , ResponseException (..)+, responseHeadersVaultKey ) where import ClassyPrelude import Network.HTTP.Types.Header-import qualified IHP.Controller.Context as Context import qualified Network.Wai-import Network.Wai (Response)+import Network.Wai (Response, Request) import qualified Control.Exception as Exception+import qualified Data.Vault.Lazy as Vault+import System.IO.Unsafe (unsafePerformIO)+import IHP.RequestVault.Helper (lookupRequestVault) -respondAndExit :: (?context :: Context.ControllerContext) => Response -> IO ()-respondAndExit response = do+-- | Simple version - just throws the response, no context needed+respondAndExit :: Response -> IO ()+respondAndExit response = Exception.throwIO (ResponseException response)+{-# INLINE respondAndExit #-}++-- | Version that adds headers from context (for render, etc.)+respondAndExitWithHeaders :: (?request :: Request) => Response -> IO ()+respondAndExitWithHeaders response = do responseWithHeaders <- addResponseHeadersFromContext response Exception.throwIO (ResponseException responseWithHeaders)-{-# INLINE respondAndExit #-}+{-# INLINE respondAndExitWithHeaders #-} -- | Add headers to current response -- | Returns a Response with headers@@ -26,21 +36,20 @@ -- addResponseHeaders :: [Header] -> Response -> Response addResponseHeaders headers = Network.Wai.mapResponseHeaders (\hs -> headers <> hs)-{-# INLINABLE addResponseHeaders #-}+{-# INLINE addResponseHeaders #-} --- | Add headers to current response, getting the headers from ControllerContext+-- | Add headers to current response, getting the headers from the request vault -- | Returns a Response with headers -- -- > addResponseHeadersFromContext response -- You probabaly want `setHeader` ---addResponseHeadersFromContext :: (?context :: Context.ControllerContext) => Response -> IO Response+addResponseHeadersFromContext :: (?request :: Request) => Response -> IO Response addResponseHeadersFromContext response = do- maybeHeaders <- Context.maybeFromContext @[Header]- let headers = fromMaybe [] maybeHeaders+ headers <- readIORef (lookupRequestVault responseHeadersVaultKey ?request) let responseWithHeaders = addResponseHeaders headers response pure responseWithHeaders-{-# INLINABLE addResponseHeadersFromContext #-}+{-# INLINE addResponseHeadersFromContext #-} -- Can be thrown from inside the action to abort the current action execution. -- Does not indicates a runtime error. It's just used for control flow management.@@ -49,3 +58,7 @@ instance Show ResponseException where show _ = "ResponseException { .. }" instance Exception ResponseException++responseHeadersVaultKey :: Vault.Key (IORef [Header])+responseHeadersVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE responseHeadersVaultKey #-}
@@ -25,19 +25,22 @@ , deleteSession , getSessionAndClear , sessionVaultKey+ , lookupSessionVault ) where -import IHP.Prelude-import IHP.Controller.RequestContext-import IHP.Controller.Context-import IHP.ModelSupport+import Prelude+import Data.ByteString (ByteString)+import Data.Maybe (isJust)+import Control.Monad (when)+import IHP.ModelSupport.Types (PrimaryKey, Id'(..))+import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.Vault.Lazy as Vault-import qualified Network.Wai as Wai+import Network.Wai import qualified Data.Serialize as Serialize import Data.Serialize (Serialize) import Data.Serialize.Text ()-import qualified Network.Wai.Session+import qualified Network.Wai.Session.Maybe import System.IO.Unsafe (unsafePerformIO) -- | Types of possible errors as a result of@@ -67,7 +70,7 @@ -- > action LogoutAction = do -- > setSession @Text "userEmail" "hi@digitallyinduced.com" ---setSession :: (?context :: ControllerContext, Serialize value)+setSession :: (?request :: Request, Serialize value) => ByteString -> value -> IO () setSession name value = sessionInsert name (Serialize.encode value) {-# INLINABLE setSession #-}@@ -84,7 +87,7 @@ -- -- If an error occurs while getting the value, the result will be 'Nothing'. getSession :: forall value- . (?context :: ControllerContext, Serialize value)+ . (?request :: Request, Serialize value) => ByteString -> IO (Maybe value) getSession name = getSessionEither name >>= \case Left _ -> pure Nothing@@ -104,7 +107,7 @@ -- > Left NotFoundError -> ... -- > Left VaultError -> ... getSessionEither :: forall value- . (?context :: ControllerContext, Serialize value)+ . (?request :: Request, Serialize value) => ByteString -> IO (Either SessionError value) getSessionEither name = sessionLookup name >>= \case Nothing -> pure $ Left NotFoundError@@ -129,7 +132,7 @@ -- > -- > deleteSession "userId" -- > userId <- getSession @Int "userId" -- Returns: Nothing-deleteSession :: (?context :: ControllerContext) => ByteString -> IO ()+deleteSession :: (?request :: Request) => ByteString -> IO () deleteSession name = sessionInsert name "" -- | Returns a value from the session, and deletes it after retrieving:@@ -137,7 +140,7 @@ -- > action SessionExampleAction = do -- > notification <- getSessionAndClear @Text "notification" getSessionAndClear :: forall value- . (?context :: ControllerContext, Serialize value)+ . (?request :: Request, Serialize value) => ByteString -> IO (Maybe value) getSessionAndClear name = do value <- getSession @value name@@ -153,20 +156,20 @@ Nothing -> fail "Failed to parse UUID" Just uuid -> pure (Id uuid) -sessionInsert :: (?context :: ControllerContext) => ByteString -> ByteString -> IO ()+sessionInsert :: (?request :: Request) => ByteString -> ByteString -> IO () sessionInsert = snd sessionVault -sessionLookup :: (?context :: ControllerContext) => ByteString -> IO (Maybe ByteString)+sessionLookup :: (?request :: Request) => ByteString -> IO (Maybe ByteString) sessionLookup = fst sessionVault -sessionVault :: (?context :: ControllerContext) => (ByteString -> IO (Maybe ByteString), ByteString -> ByteString -> IO ())-sessionVault = case vaultLookup of+sessionVault :: (?request :: Request) => (ByteString -> IO (Maybe ByteString), ByteString -> ByteString -> IO ())+sessionVault = case lookupSessionVault ?request of Just session -> session Nothing -> error "sessionInsert: The session vault is missing in the request"- where- RequestContext { request } = ?context.requestContext- vaultLookup = Vault.lookup sessionVaultKey request.vault -sessionVaultKey :: Vault.Key (Network.Wai.Session.Session IO ByteString ByteString)+lookupSessionVault :: Request -> Maybe (ByteString -> IO (Maybe ByteString), ByteString -> ByteString -> IO ())+lookupSessionVault request = Vault.lookup sessionVaultKey request.vault++sessionVaultKey :: Vault.Key (Network.Wai.Session.Maybe.Session IO ByteString ByteString) sessionVaultKey = unsafePerformIO Vault.newKey {-# NOINLINE sessionVaultKey #-}
@@ -1,3 +1,4 @@+{-# OPTIONS_HADDOCK not-home, hide #-} module IHP.ControllerPrelude ( module IHP.Prelude , module IHP.ControllerSupport@@ -15,18 +16,17 @@ , module IHP.FrameworkConfig , module IHP.QueryBuilder , module IHP.Fetch+ , module IHP.FetchPipelined , module IHP.FetchRelated , module Data.Aeson , module Network.Wai.Parse , module IHP.RouterSupport , module IHP.ValidationSupport , module IHP.AutoRefresh- , module IHP.Mail- , module IHP.FlashMessages.Types- , module IHP.FlashMessages.ControllerFunctions+ , module IHP.FlashMessages , module IHP.Controller.Context , module IHP.Modal.Types- , module IHP.Modal.ControllerFunctions+ , setModal , module IHP.Controller.Layout , module IHP.Job.Types , module IHP.LoginSupport.Helper.Controller@@ -46,7 +46,6 @@ import IHP.Controller.AccessDenied import IHP.Controller.NotFound import IHP.Controller.Session-import IHP.Controller.RequestContext import IHP.Controller.BasicAuth import IHP.Controller.Cookie import IHP.ControllerSupport@@ -56,23 +55,24 @@ import IHP.FrameworkConfig import IHP.QueryBuilder import IHP.Fetch+import IHP.FetchPipelined import IHP.FetchRelated import Data.Aeson hiding (Success) import Network.Wai.Parse (FileInfo(..)) import IHP.RouterSupport hiding (get, post) import IHP.Controller.Redirect-import IHP.Mail (sendMail) import Database.PostgreSQL.Simple.Types (Only (..))-import IHP.FlashMessages.Types-import IHP.FlashMessages.ControllerFunctions+import IHP.FlashMessages import IHP.Controller.Context import IHP.Controller.Layout import IHP.Modal.Types-import IHP.Modal.ControllerFunctions+import qualified IHP.Modal.ControllerFunctions as Modal+import IHP.ViewSupport (View)+import qualified IHP.ViewSupport as ViewSupport import IHP.Job.Types-import IHP.AutoRefresh (initAutoRefresh, autoRefresh)+import IHP.AutoRefresh (autoRefresh) import IHP.LoginSupport.Helper.Controller import IHP.PageHead.ControllerFunctions@@ -86,3 +86,10 @@ import IHP.Pagination.ControllerFunctions import IHP.HSX.QQ (hsx) import IHP.HSX.ToHtml ()++-- | Renders a view and stores it as modal HTML in the context for later rendering.+--+-- > setModal MyModalView { .. }+--+setModal :: (?context :: ControllerContext, ?request :: Request, View view) => view -> IO ()+setModal view = let ?view = view in Modal.setModal (ViewSupport.html view)
@@ -7,43 +7,48 @@ , getRequestPath , getRequestPathAndQuery , getHeader-, RequestContext (RequestContext) , request , requestHeaders , getFiles , Controller (..) , runAction-, createRequestContext-, ControllerContext+, Context.ControllerContext , InitControllerContext (..) , runActionWithNewContext , newContextForAction , respondAndExit+, respondAndExitWithHeaders , jumpToAction , requestBodyJSON , startWebSocketApp , startWebSocketAppAndFailOnHTTP , setHeader , getAppConfig+, Respond+, Request+, rlsContextVaultKey+, setupActionContext ) where -import ClassyPrelude+import Prelude+import Data.IORef (IORef, modifyIORef', readIORef)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as LBS+import Data.Maybe (fromMaybe)+import Control.Exception.Safe (SomeException, fromException, try, catch, throwIO)+import Data.Typeable (Typeable) import IHP.HaskellSupport-import Network.Wai (Request, ResponseReceived, responseLBS, requestHeaders)+import Network.Wai import qualified Network.HTTP.Types as HTTP-import qualified Network.Wai import IHP.ModelSupport-import IHP.ApplicationContext (ApplicationContext (..)) import Network.Wai.Parse as WaiParse import qualified Data.ByteString.Lazy-import qualified IHP.Controller.RequestContext as RequestContext-import IHP.Controller.RequestContext (RequestContext, Respond)+import Wai.Request.Params.Middleware (Respond) import qualified Data.CaseInsensitive import qualified IHP.ErrorController as ErrorController import qualified Data.Typeable as Typeable-import IHP.FrameworkConfig (FrameworkConfig (..), ConfigProvider(..))+import IHP.FrameworkConfig.Types (FrameworkConfig (..), ConfigProvider) import qualified IHP.Controller.Context as Context-import IHP.Controller.Context (ControllerContext(ControllerContext), customFieldsRef) import IHP.Controller.Response import Network.HTTP.Types.Header import qualified Data.Aeson as Aeson@@ -51,17 +56,24 @@ import qualified Network.WebSockets as WebSockets import qualified IHP.WebSocket as WebSockets import qualified Data.TMap as TypeMap+import IHP.RequestVault.ModelContext+import IHP.ActionType (setActionType, actionTypeVaultKey, ActionType(..))+import IHP.RequestVault.Helper (lookupRequestVault)+import qualified IHP.Environment as Environment+import qualified Data.Vault.Lazy as Vault+import qualified Data.Text as Text+import System.IO.Unsafe (unsafePerformIO) type Action' = IO ResponseReceived class (Show controller, Eq controller) => Controller controller where- beforeAction :: (?context :: ControllerContext, ?modelContext :: ModelContext, ?theAction :: controller) => IO ()+ beforeAction :: (?context :: Context.ControllerContext, ?modelContext :: ModelContext, ?theAction :: controller, ?respond :: Respond, ?request :: Request) => IO () beforeAction = pure () {-# INLINABLE beforeAction #-}- action :: (?context :: ControllerContext, ?modelContext :: ModelContext, ?theAction :: controller) => controller -> IO ()+ action :: (?context :: Context.ControllerContext, ?modelContext :: ModelContext, ?theAction :: controller, ?respond :: Respond, ?request :: Request) => controller -> IO () class InitControllerContext application where- initContext :: (?modelContext :: ModelContext, ?requestContext :: RequestContext, ?applicationContext :: ApplicationContext, ?context :: ControllerContext) => IO ()+ initContext :: (?modelContext :: ModelContext, ?request :: Request, ?respond :: Respond, ?context :: Context.ControllerContext) => IO () initContext = pure () {-# INLINABLE initContext #-} @@ -69,10 +81,10 @@ initContext = pure () {-# INLINE runAction #-}-runAction :: forall controller. (Controller controller, ?context :: ControllerContext, ?modelContext :: ModelContext, ?applicationContext :: ApplicationContext, ?requestContext :: RequestContext) => controller -> IO ResponseReceived+runAction :: forall controller. (Controller controller, ?context :: Context.ControllerContext, ?modelContext :: ModelContext, ?respond :: Respond) => controller -> IO ResponseReceived runAction controller = do let ?theAction = controller- let respond = ?context.requestContext.respond+ let ?request = ?context.request let doRunAction = do authenticatedModelContext <- prepareRLSIfNeeded ?modelContext@@ -82,48 +94,73 @@ (action controller) ErrorController.handleNoResponseReturned controller - let handleResponseException (ResponseException response) = respond response-- doRunAction `catches` [ Handler handleResponseException, Handler (\exception -> ErrorController.displayException exception controller "")]+ doRunAction `catch` \(exception :: SomeException) -> case fromException exception of+ Just (ResponseException response) -> ?respond response+ Nothing -> ErrorController.displayException exception controller "" {-# INLINE newContextForAction #-} newContextForAction :: forall application controller . ( Controller controller- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext+ , ?request :: Request+ , ?respond :: Respond , InitControllerContext application , ?application :: application , Typeable application , Typeable controller )- => controller -> IO (Either (IO ResponseReceived) ControllerContext)+ => controller -> IO (Either (IO ResponseReceived) Context.ControllerContext) newContextForAction controller = do- let ?modelContext = ?applicationContext.modelContext- let ?requestContext = ?context+ let ?modelContext = ?request.modelContext controllerContext <- Context.newControllerContext let ?context = controllerContext Context.putContext ?application- Context.putContext (Context.ActionType (Typeable.typeOf controller)) try (initContext @application) >>= \case Left (exception :: SomeException) -> do pure $ Left $ case fromException exception of- Just (ResponseException response) ->- let respond = ?context.requestContext.respond- in respond response+ Just (ResponseException response) -> ?respond response Nothing -> ErrorController.displayException exception controller " while calling initContext" Right _ -> pure $ Right ?context +-- | Shared request context setup, specialized once per application type.+-- Takes a pre-computed TypeRep to avoid per-controller-type code duplication.+-- NOINLINE ensures GHC compiles one copy shared across all controllers.+--+-- Returns @(controllerContext, Nothing)@ on success, or+-- @(controllerContext, Just exception)@ if 'initContext' failed.+-- The context is always returned so callers can use it for error rendering.+{-# NOINLINE setupActionContext #-}+setupActionContext+ :: forall application+ . ( InitControllerContext application+ , ?application :: application+ , Typeable application+ )+ => Typeable.TypeRep -> Request -> Respond+ -> IO (Context.ControllerContext, Maybe SomeException)+setupActionContext controllerTypeRep waiRequest waiRespond = do+ let !request' = waiRequest { vault = Vault.insert actionTypeVaultKey (ActionType controllerTypeRep) waiRequest.vault }+ let ?request = request'+ let ?respond = waiRespond+ let ?modelContext = request'.modelContext+ controllerContext <- Context.newControllerContext+ let ?context = controllerContext+ Context.putContext ?application+ try (initContext @application) >>= \case+ Left exception -> pure (?context, Just exception)+ Right _ -> pure (?context, Nothing)+ {-# INLINE runActionWithNewContext #-}-runActionWithNewContext :: forall application controller. (Controller controller, ?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, Typeable controller) => controller -> IO ResponseReceived+runActionWithNewContext :: forall application controller. (Controller controller, ?request :: Request, ?respond :: Respond, InitControllerContext application, ?application :: application, Typeable application, Typeable controller) => controller -> IO ResponseReceived runActionWithNewContext controller = do+ let request' = setActionType controller ?request+ let ?request = request' contextOrResponse <- newContextForAction controller case contextOrResponse of Left response -> response Right context -> do- let ?modelContext = ?applicationContext.modelContext- let ?requestContext = ?context+ let ?modelContext = requestModelContext ?request let ?context = context runAction controller @@ -131,19 +168,24 @@ -- the prepared RowLevelSecurityContext from the controller context into the ModelContext. -- -- If row leve security wasn't enabled, this will just return the current model context.-prepareRLSIfNeeded :: (?context :: ControllerContext) => ModelContext -> IO ModelContext+prepareRLSIfNeeded :: (?request :: Request) => ModelContext -> IO ModelContext prepareRLSIfNeeded modelContext = do- rowLevelSecurityContext <- Context.maybeFromContext+ rowLevelSecurityContext <- readIORef (lookupRequestVault rlsContextVaultKey ?request) case rowLevelSecurityContext of Just context -> pure modelContext { rowLevelSecurity = Just context } Nothing -> pure modelContext+{-# INLINE prepareRLSIfNeeded #-} +rlsContextVaultKey :: Vault.Key (IORef (Maybe RowLevelSecurityContext))+rlsContextVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE rlsContextVaultKey #-}+ {-# INLINE startWebSocketApp #-}-startWebSocketApp :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => webSocketApp -> IO ResponseReceived -> Network.Wai.Application-startWebSocketApp initialState onHTTP request respond = do- let ?modelContext = ?applicationContext.modelContext- requestContext <- createRequestContext ?applicationContext request respond- let ?requestContext = requestContext+startWebSocketApp :: forall webSocketApp application. (?request :: Request, ?respond :: Respond, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => webSocketApp -> IO ResponseReceived -> Application+startWebSocketApp initialState onHTTP waiRequest waiRespond = do+ let ?modelContext = requestModelContext ?request+ let ?request = waiRequest+ let ?respond = waiRespond let handleConnection pendingConnection = do connection <- WebSockets.acceptRequest pendingConnection@@ -154,45 +196,40 @@ Context.putContext ?application try (initContext @application) >>= \case- Left (exception :: SomeException) -> putStrLn $ "Unexpected exception in initContext, " <> tshow exception+ Left (exception :: SomeException) -> putStrLn $ "Unexpected exception in initContext, " <> show exception Right context -> do WebSockets.startWSApp initialState connection let connectionOptions = WebSockets.connectionOptions @webSocketApp - request+ waiRequest |> WebSockets.websocketsApp connectionOptions handleConnection |> \case- Just response -> respond response+ Just response -> waiRespond response Nothing -> onHTTP {-# INLINE startWebSocketAppAndFailOnHTTP #-}-startWebSocketAppAndFailOnHTTP :: forall webSocketApp application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => webSocketApp -> Network.Wai.Application-startWebSocketAppAndFailOnHTTP initialState = startWebSocketApp @webSocketApp @application initialState (respond $ responseLBS HTTP.status400 [(hContentType, "text/plain")] "This endpoint is only available via a WebSocket")- where- respond = ?context.respond+startWebSocketAppAndFailOnHTTP :: forall webSocketApp application. (?request :: Request, ?respond :: Respond, InitControllerContext application, ?application :: application, Typeable application, WebSockets.WSApp webSocketApp) => webSocketApp -> Application+startWebSocketAppAndFailOnHTTP initialState = startWebSocketApp @webSocketApp @application initialState (?respond $ responseLBS HTTP.status400 [(hContentType, "text/plain")] "This endpoint is only available via a WebSocket") -jumpToAction :: forall action. (Controller action, ?context :: ControllerContext, ?modelContext :: ModelContext) => action -> IO ()+jumpToAction :: forall action. (Controller action, ?context :: Context.ControllerContext, ?modelContext :: ModelContext, ?respond :: Respond, ?request :: Request) => action -> IO () jumpToAction theAction = do let ?theAction = theAction beforeAction @action action theAction -{-# INLINE getRequestBody #-}-getRequestBody :: (?context :: ControllerContext) => IO LByteString+getRequestBody :: (?request :: Request) => IO LBS.ByteString getRequestBody =- case ?context.requestContext.requestBody of- RequestContext.JSONBody { rawPayload } -> pure rawPayload- _ -> Network.Wai.lazyRequestBody request+ pure ?request.parsedBody.rawPayload -- | Returns the request path, e.g. @/Users@ or @/CreateUser@-getRequestPath :: (?context :: ControllerContext) => ByteString-getRequestPath = request.rawPathInfo+getRequestPath :: (?request :: Request) => ByteString+getRequestPath = ?request.rawPathInfo {-# INLINABLE getRequestPath #-} -- | Returns the request path and the query params, e.g. @/ShowUser?userId=9bd6b37b-2e53-40a4-bb7b-fdba67d6af42@-getRequestPathAndQuery :: (?context :: ControllerContext) => ByteString-getRequestPathAndQuery = request.rawPathInfo <> request.rawQueryString+getRequestPathAndQuery :: (?request :: Request) => ByteString+getRequestPathAndQuery = ?request.rawPathInfo <> ?request.rawQueryString {-# INLINABLE getRequestPathAndQuery #-} -- | Returns a header value for a given header name. Returns Nothing if not found@@ -205,60 +242,60 @@ -- >>> getHeader "X-My-Custom-Header" -- Nothing ---getHeader :: (?context :: ControllerContext) => ByteString -> Maybe ByteString-getHeader name = lookup (Data.CaseInsensitive.mk name) request.requestHeaders+getHeader :: (?request :: Request) => ByteString -> Maybe ByteString+getHeader name = lookup (Data.CaseInsensitive.mk name) ?request.requestHeaders {-# INLINABLE getHeader #-} -- | Set a header value for a given header name. -- -- >>> setHeader ("Content-Language", "en") ---setHeader :: (?context :: ControllerContext) => Header -> IO ()+setHeader :: (?request :: Request) => Header -> IO () setHeader header = do- maybeHeaders <- Context.maybeFromContext @[Header]- let headers = fromMaybe [] maybeHeaders- Context.putContext (header : headers)+ let headersRef = lookupRequestVault responseHeadersVaultKey ?request+ modifyIORef' headersRef (header :) {-# INLINABLE setHeader #-} -- | Returns the current HTTP request. -- -- See https://hackage.haskell.org/package/wai-3.2.2.1/docs/Network-Wai.html#t:Request-request :: (?context :: ControllerContext) => Network.Wai.Request-request = requestContext.request+request :: (?request :: Request) => Request+request = ?request {-# INLINE request #-} {-# INLINE getFiles #-}-getFiles :: (?context :: ControllerContext) => [File Data.ByteString.Lazy.ByteString]+getFiles :: (?request :: Request) => [File Data.ByteString.Lazy.ByteString] getFiles =- case requestContext.requestBody of- RequestContext.FormBody { files } -> files+ case ?request.parsedBody of+ FormBody { files } -> files _ -> [] -requestContext :: (?context :: ControllerContext) => RequestContext-requestContext = ?context.requestContext-{-# INLINE requestContext #-}--requestBodyJSON :: (?context :: ControllerContext) => Aeson.Value+requestBodyJSON :: (?request :: Request) => IO Aeson.Value requestBodyJSON =- case ?context.requestContext.requestBody of- RequestContext.JSONBody { jsonPayload = Just value } -> value- _ -> error "Expected JSON body"--{-# INLINE createRequestContext #-}-createRequestContext :: ApplicationContext -> Request -> Respond -> IO RequestContext-createRequestContext ApplicationContext { frameworkConfig } request respond = do- let contentType = lookup hContentType (requestHeaders request)- requestBody <- case contentType of- "application/json" -> do- rawPayload <- Network.Wai.lazyRequestBody request- let jsonPayload = Aeson.decode rawPayload- pure RequestContext.JSONBody { jsonPayload, rawPayload }- _ -> do- (params, files) <- WaiParse.parseRequestBodyEx frameworkConfig.parseRequestBodyOptions WaiParse.lbsBackEnd request- pure RequestContext.FormBody { .. }-- pure RequestContext.RequestContext { request, respond, requestBody, frameworkConfig }-+ case ?request.parsedBody of+ JSONBody { jsonPayload = Just value } -> pure value+ JSONBody { jsonPayload = Nothing, rawPayload } -> do+ let isDev = ?request.frameworkConfig.environment == Environment.Development+ let errorMessage = "Expected JSON body, but could not decode the request body"+ <> (if LBS.null rawPayload+ then ". The request body is empty."+ else if isDev+ then ". The raw request body was: " <> truncatePayload rawPayload+ else ".")+ throwResponseException $ responseLBS HTTP.status400 [(hContentType, "application/json")] $+ Aeson.encode $ Aeson.object [("error", Aeson.String errorMessage)]+ where+ truncatePayload payload =+ let shown = show payload+ maxLen = 200+ in if length shown > maxLen+ then Text.pack (take maxLen shown) <> "... (truncated)"+ else Text.pack shown+ FormBody {} ->+ throwResponseException $ responseLBS HTTP.status400 [(hContentType, "application/json")] $+ Aeson.encode $ Aeson.object [("error", Aeson.String "Expected JSON body, but the request has a form content type. Make sure to set 'Content-Type: application/json' in the request header.")]+ where+ throwResponseException response = throwIO (ResponseException response) -- | Returns a custom config parameter --
@@ -1,187 +0,0 @@-module IHP.DataSync.ChangeNotifications-( channelName-, ChangeNotification (..)-, Change (..)-, ChangeSet (..)-, createNotificationFunction-, installTableChangeTriggers-, makeCachedInstallTableChangeTriggers-, retrieveChanges-) where--import IHP.Prelude-import qualified Database.PostgreSQL.Simple as PG-import IHP.ModelSupport-import Data.String.Interpolate.IsString (i)-import Data.Aeson-import Data.Aeson.TH-import qualified IHP.DataSync.RowLevelSecurity as RLS-import qualified Data.Set as Set-import qualified Data.UUID as UUID--data ChangeNotification- = DidInsert { id :: !UUID }- | DidUpdate { id :: !UUID, changeSet :: !ChangeSet }- | DidUpdateLarge { id :: !UUID, payloadId :: !UUID }- | DidDelete { id :: !UUID }- deriving (Eq, Show)--data ChangeSet- = InlineChangeSet { changeSet :: ![Change] } -- | When the patch fits into the 8000 bytes limit of @pg_notify@- | ExternalChangeSet { largePgNotificationId :: !UUID } -- | The patch is over 8000 bytes, so we have stored it in the @large_pg_notifications@ table- deriving (Eq, Show)--data Change = Change- { col :: !Text- , new :: !Value- } deriving (Eq, Show)---- | Returns the sql code to set up a database trigger. Mainly used by 'watchInsertOrUpdateTable'.-createNotificationFunction :: RLS.TableWithRLS -> PG.Query-createNotificationFunction table = [i|- DO $$- BEGIN- CREATE FUNCTION "#{functionName}"() RETURNS TRIGGER AS $BODY$- DECLARE- payload TEXT;- large_pg_notification_id UUID;- changeset JSON;- BEGIN- CASE TG_OP- WHEN 'UPDATE' THEN- SELECT coalesce(json_agg(row_to_json(t)), '[]'::json)- FROM (- SELECT pre.key AS "col", post.value AS "new"- FROM jsonb_each(to_jsonb(OLD)) AS pre- CROSS JOIN jsonb_each(to_jsonb(NEW)) AS post- WHERE pre.key = post.key AND pre.value IS DISTINCT FROM post.value- ) t INTO changeset;- payload := json_build_object(- 'UPDATE', NEW.id::text,- 'CHANGESET', changeset- )::text;- IF octet_length(payload) > 7800 THEN- INSERT INTO large_pg_notifications (payload) VALUES (changeset) RETURNING id INTO large_pg_notification_id;- payload := json_build_object(- 'UPDATE', NEW.id::text,- 'CHANGESET', large_pg_notification_id::text- )::text;- DELETE FROM large_pg_notifications WHERE created_at < CURRENT_TIMESTAMP - interval '30s';- END IF;- PERFORM pg_notify(- '#{channelName table}',- payload- );- WHEN 'DELETE' THEN- PERFORM pg_notify(- '#{channelName table}',- (json_build_object('DELETE', OLD.id)::text)- );- WHEN 'INSERT' THEN- PERFORM pg_notify(- '#{channelName table}',- json_build_object('INSERT', NEW.id)::text- );- END CASE;- RETURN new;- END;- $BODY$ language plpgsql;- DROP TRIGGER IF EXISTS "#{insertTriggerName}" ON "#{tableName}";- DROP TRIGGER IF EXISTS "#{updateTriggerName}" ON "#{tableName}";- DROP TRIGGER IF EXISTS "#{deleteTriggerName}" ON "#{tableName}";--- CREATE TRIGGER "#{insertTriggerName}" AFTER INSERT ON "#{tableName}" FOR EACH ROW EXECUTE PROCEDURE "#{functionName}"();- CREATE TRIGGER "#{updateTriggerName}" AFTER UPDATE ON "#{tableName}" FOR EACH ROW EXECUTE PROCEDURE "#{functionName}"();- CREATE TRIGGER "#{deleteTriggerName}" AFTER DELETE ON "#{tableName}" FOR EACH ROW EXECUTE PROCEDURE "#{functionName}"();- EXCEPTION- WHEN duplicate_function THEN- null;- - IF NOT EXISTS (- SELECT FROM pg_catalog.pg_class c- JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace- WHERE c.relname = 'large_pg_notifications'- AND n.nspname = 'public'- ) THEN- CREATE UNLOGGED TABLE large_pg_notifications (- id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,- payload TEXT DEFAULT NULL,- created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL- );- CREATE INDEX large_pg_notifications_created_at_index ON large_pg_notifications (created_at);- END IF;- END; $$-|]-- where- tableName = table.tableName-- functionName = "notify_did_change_" <> tableName- insertTriggerName = "did_insert_" <> tableName- updateTriggerName = "did_update_" <> tableName- deleteTriggerName = "did_delete_" <> tableName--installTableChangeTriggers :: (?modelContext :: ModelContext) => RLS.TableWithRLS -> IO ()-installTableChangeTriggers tableNameRLS = do- withoutQueryLogging -- This spams the log way to much- (sqlExec (createNotificationFunction tableNameRLS) ())- pure ()--makeCachedInstallTableChangeTriggers :: (?modelContext :: ModelContext) => IO (RLS.TableWithRLS -> IO ())-makeCachedInstallTableChangeTriggers = do- tables <- newIORef Set.empty- pure \tableName -> do- triggersInstalled <- Set.member tableName <$> readIORef tables-- unless triggersInstalled do- installTableChangeTriggers tableName- modifyIORef' tables (Set.insert tableName)---- | Returns the event name of the event that the pg notify trigger dispatches-channelName :: RLS.TableWithRLS -> ByteString-channelName table = "did_change_" <> (cs $ table.tableName)---instance FromJSON ChangeNotification where- parseJSON = withObject "ChangeNotification" $ \values -> insert values <|> update values <|> delete values- where- insert values = do- id <- values .: "INSERT"- pure DidInsert { id }- update values = do- id <- values .: "UPDATE"- changeSet <- values .: "CHANGESET"- pure $ DidUpdate id changeSet- delete values = DidDelete <$> values .: "DELETE"--instance FromJSON ChangeSet where- parseJSON array@(Array v) = do- changeSet <- parseJSON array- pure InlineChangeSet { changeSet }- parseJSON (String id) = do- case UUID.fromText id of- Just largePgNotificationId -> pure ExternalChangeSet { largePgNotificationId }- Nothing -> fail "Invalid UUID"--instance FromJSON Change where- parseJSON = withObject "Change" $ \values -> do- col <- values .: "col"- new <- values .: "new"- pure Change { .. }--- | The @pg_notify@ function has a payload limit of 8000 bytes. When a record update is larger than the payload size--- we store the patch in the @large_pg_notifications@ table and pass over the id to the patch.------ This function retrieves the patch from the @large_pg_notifications@ table, or directly returns the patch--- when it's less than 8000 bytes.-retrieveChanges :: (?modelContext :: ModelContext) => ChangeSet -> IO [Change]-retrieveChanges InlineChangeSet { changeSet } = pure changeSet-retrieveChanges ExternalChangeSet { largePgNotificationId } = do- (payload :: ByteString) <- sqlQueryScalar "SELECT payload FROM large_pg_notifications WHERE id = ? LIMIT 1" (PG.Only largePgNotificationId)- case eitherDecodeStrict' payload of- Left e -> fail e- Right result -> pure result--$(deriveToJSON defaultOptions 'Change)-$(deriveToJSON defaultOptions 'InlineChangeSet)-$(deriveToJSON defaultOptions 'DidInsert)
@@ -1,26 +0,0 @@-{-# LANGUAGE UndecidableInstances #-}-module IHP.DataSync.Controller where--import IHP.ControllerPrelude hiding (OrderByClause)--import IHP.DataSync.Types-import IHP.DataSync.RowLevelSecurity-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified IHP.DataSync.ChangeNotifications as ChangeNotifications-import IHP.DataSync.ControllerImpl (runDataSyncController, cleanupAllSubscriptions)-import IHP.DataSync.DynamicQueryCompiler (camelCaseRenamer)--instance (- PG.ToField (PrimaryKey (GetTableName CurrentUserRecord))- , Show (PrimaryKey (GetTableName CurrentUserRecord))- , HasNewSessionUrl CurrentUserRecord- , Typeable CurrentUserRecord- , HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- ) => WSApp DataSyncController where- initialState = DataSyncController-- run = do- ensureRLSEnabled <- makeCachedEnsureRLSEnabled- installTableChangeTriggers <- ChangeNotifications.makeCachedInstallTableChangeTriggers- runDataSyncController ensureRLSEnabled installTableChangeTriggers (receiveData @ByteString) sendJSON (\_ _ -> pure ()) (\_ -> camelCaseRenamer)- onClose = cleanupAllSubscriptions
@@ -1,533 +0,0 @@-{-# LANGUAGE UndecidableInstances #-}-module IHP.DataSync.ControllerImpl where--import IHP.ControllerPrelude hiding (OrderByClause)-import qualified Control.Exception.Safe as Exception-import qualified IHP.Log as Log-import qualified Data.Aeson as Aeson-import qualified Data.Aeson.Key as Aeson--import Data.Aeson.TH-import qualified Database.PostgreSQL.Simple as PG-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Data.HashMap.Strict as HashMap-import qualified Data.UUID.V4 as UUID-import qualified Control.Concurrent.MVar as MVar-import IHP.DataSync.Types-import IHP.DataSync.RowLevelSecurity-import IHP.DataSync.DynamicQuery-import IHP.DataSync.DynamicQueryCompiler-import qualified IHP.DataSync.ChangeNotifications as ChangeNotifications-import IHP.DataSync.REST.Controller (aesonValueToPostgresValue)-import qualified Data.ByteString.Char8 as ByteString-import qualified IHP.PGListener as PGListener-import IHP.ApplicationContext-import qualified Data.Set as Set-import qualified Data.Pool as Pool---$(deriveFromJSON defaultOptions ''DataSyncMessage)-$(deriveToJSON defaultOptions 'DataSyncResult)--type EnsureRLSEnabledFn = Text -> IO TableWithRLS-type InstallTableChangeTriggerFn = TableWithRLS -> IO ()-type SendJSONFn = DataSyncResponse -> IO ()-type HandleCustomMessageFn = (DataSyncResponse -> IO ()) -> DataSyncMessage -> IO ()--runDataSyncController ::- ( HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- , ?applicationContext :: ApplicationContext- , ?context :: ControllerContext- , ?modelContext :: ModelContext- , ?state :: IORef DataSyncController- , PG.ToField (PrimaryKey (GetTableName CurrentUserRecord))- , Typeable CurrentUserRecord- , HasNewSessionUrl CurrentUserRecord- , Show (PrimaryKey (GetTableName CurrentUserRecord))- ) => EnsureRLSEnabledFn -> InstallTableChangeTriggerFn -> IO ByteString -> SendJSONFn -> HandleCustomMessageFn -> (Text -> Renamer) -> IO ()-runDataSyncController ensureRLSEnabled installTableChangeTriggers receiveData sendJSON handleCustomMessage renamer = do- setState DataSyncReady { subscriptions = HashMap.empty, transactions = HashMap.empty, asyncs = [] }-- let handleMessage :: DataSyncMessage -> IO () = buildMessageHandler ensureRLSEnabled installTableChangeTriggers sendJSON handleCustomMessage renamer-- forever do- message <- Aeson.eitherDecodeStrict' <$> receiveData-- case message of- Right decodedMessage -> do- let requestId = decodedMessage.requestId-- Exception.mask \restore -> do- -- Handle the messages in an async way- -- This increases throughput as multiple queries can be fetched- -- in parallel- handlerProcess <- async $ restore do- result <- Exception.try (handleMessage decodedMessage)-- case result of- Left (e :: Exception.SomeException) -> do- let errorMessage = case fromException e of- Just (enhancedSqlError :: EnhancedSqlError) -> cs (enhancedSqlError.sqlError.sqlErrorMsg)- Nothing -> cs (displayException e)- Log.error (tshow e)- sendJSON DataSyncError { requestId, errorMessage }- Right result -> pure ()-- atomicModifyIORef'' ?state (\state -> state |> modify #asyncs (handlerProcess:))- pure ()- Left errorMessage -> sendJSON FailedToDecodeMessageError { errorMessage = cs errorMessage }-{-# INLINE runDataSyncController #-}---buildMessageHandler ::- ( HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- , ?applicationContext :: ApplicationContext- , ?context :: ControllerContext- , ?modelContext :: ModelContext- , ?state :: IORef DataSyncController- , PG.ToField (PrimaryKey (GetTableName CurrentUserRecord))- , Typeable CurrentUserRecord- , HasNewSessionUrl CurrentUserRecord- , Show (PrimaryKey (GetTableName CurrentUserRecord))- )- => EnsureRLSEnabledFn -> InstallTableChangeTriggerFn -> SendJSONFn -> HandleCustomMessageFn -> (Text -> Renamer) -> (DataSyncMessage -> IO ())-buildMessageHandler ensureRLSEnabled installTableChangeTriggers sendJSON handleCustomMessage renamer = handleMessage- where- pgListener = ?applicationContext.pgListener- handleMessage :: DataSyncMessage -> IO ()- handleMessage DataSyncQuery { query, requestId, transactionId } = do- ensureRLSEnabled (query.table)-- let (theQuery, theParams) = compileQueryWithRenamer (renamer query.table) query-- rawResult :: [[Field]] <- sqlQueryWithRLSAndTransactionId transactionId theQuery theParams- let result = map (map (renameField (renamer query.table))) rawResult-- sendJSON DataSyncResult { result, requestId }- - handleMessage CreateDataSubscription { query, requestId } = do- ensureBelowSubscriptionsLimit-- tableNameRLS <- ensureRLSEnabled (query.table)-- subscriptionId <- UUID.nextRandom-- -- Allocate the close handle as early as possible- -- to make DeleteDataSubscription calls succeed even when the DataSubscription is- -- not fully set up yet- close <- MVar.newEmptyMVar- atomicModifyIORef'' ?state (\state -> state |> modify #subscriptions (HashMap.insert subscriptionId close))-- let (theQuery, theParams) = compileQueryWithRenamer (renamer query.table) query-- rawResult :: [[Field]] <- sqlQueryWithRLS theQuery theParams- let result = map (map (renameField (renamer query.table))) rawResult-- let tableName = query.table-- -- We need to keep track of all the ids of entities we're watching to make- -- sure that we only send update notifications to clients that can actually- -- access the record (e.g. if a RLS policy denies access)- let watchedRecordIds = recordIds rawResult-- -- Store it in IORef as an INSERT requires us to add an id- watchedRecordIdsRef <- newIORef (Set.fromList watchedRecordIds)-- -- Make sure the database triggers are there- installTableChangeTriggers tableNameRLS-- let callback notification = case notification of- ChangeNotifications.DidInsert { id } -> do- -- The new record could not be accessible to the current user with a RLS policy- -- E.g. it could be a new record in a 'projects' table, but the project belongs- -- to a different user, and thus the current user should not be able to see it.- --- -- The new record could also be not part of the WHERE condition of the initial query.- -- Therefore we need to use the subscriptions WHERE condition to fetch the new record here.- --- -- To honor the RLS policies we therefore need to fetch the record as the current user- -- If the result set is empty, we know the record is not accesible to us- newRecord :: [[Field]] <- sqlQueryWithRLS ("SELECT * FROM (" <> theQuery <> ") AS records WHERE records.id = ? LIMIT 1") (theParams <> [PG.toField id])-- case headMay newRecord of- Just rawRecord -> do- -- Add the new record to 'watchedRecordIdsRef'- -- Otherwise the updates and deletes will not be dispatched to the client- modifyIORef' watchedRecordIdsRef (Set.insert id)-- let record = map (renameField (renamer tableName)) rawRecord- sendJSON DidInsert { subscriptionId, record }- Nothing -> pure ()- ChangeNotifications.DidUpdate { id, changeSet } -> do- -- Only send the notifcation if the deleted record was part of the initial- -- results set- isWatchingRecord <- Set.member id <$> readIORef watchedRecordIdsRef- when isWatchingRecord do- -- The updated record could not be part of the query result set anymore- -- E.g. if it's not matched anymore by the WHERE condition after the update- [(PG.Only isRecordInResultSet)] <- sqlQueryWithRLS ("SELECT EXISTS(SELECT * FROM (" <> theQuery <> ") AS records WHERE records.id = ? LIMIT 1)") (theParams <> [PG.toField id])-- changes <- ChangeNotifications.retrieveChanges changeSet- if isRecordInResultSet- then sendJSON DidUpdate { subscriptionId, id, changeSet = changesToValue (renamer tableName) changes }- else sendJSON DidDelete { subscriptionId, id }- ChangeNotifications.DidDelete { id } -> do- -- Only send the notifcation if the deleted record was part of the initial- -- results set- isWatchingRecord <- Set.member id <$> readIORef watchedRecordIdsRef- when isWatchingRecord do- sendJSON DidDelete { subscriptionId, id }-- let subscribe = PGListener.subscribeJSON (ChangeNotifications.channelName tableNameRLS) callback pgListener- let unsubscribe subscription = PGListener.unsubscribe subscription pgListener-- Exception.bracket subscribe unsubscribe \channelSubscription -> do- sendJSON DidCreateDataSubscription { subscriptionId, requestId, result }-- MVar.takeMVar close-- handleMessage CreateCountSubscription { query, requestId } = do- ensureBelowSubscriptionsLimit-- tableNameRLS <- ensureRLSEnabled query.table-- subscriptionId <- UUID.nextRandom-- -- Allocate the close handle as early as possible- -- to make DeleteDataSubscription calls succeed even when the CountSubscription is- -- not fully set up yet- close <- MVar.newEmptyMVar- atomicModifyIORef'' ?state (\state -> state |> modify #subscriptions (HashMap.insert subscriptionId close))-- let (theQuery, theParams) = compileQueryWithRenamer (renamer query.table) query-- let countQuery = "SELECT COUNT(*) FROM (" <> theQuery <> ") AS _inner"-- let- unpackResult :: [(Only Int)] -> Int- unpackResult [(Only value)] = value- unpackResult otherwise = error "DataSync.unpackResult: Expected INT, but got something else"-- count <- unpackResult <$> sqlQueryWithRLS countQuery theParams- countRef <- newIORef count-- installTableChangeTriggers tableNameRLS-- let- callback :: ChangeNotifications.ChangeNotification -> IO ()- callback _ = do- newCount <- unpackResult <$> sqlQueryWithRLS countQuery theParams- lastCount <- readIORef countRef-- when (newCount /= count) (sendJSON DidChangeCount { subscriptionId, count = newCount })-- let subscribe = PGListener.subscribeJSON (ChangeNotifications.channelName tableNameRLS) callback pgListener- let unsubscribe subscription = PGListener.unsubscribe subscription pgListener-- Exception.bracket subscribe unsubscribe \channelSubscription -> do- sendJSON DidCreateCountSubscription { subscriptionId, requestId, count }-- MVar.takeMVar close-- handleMessage DeleteDataSubscription { requestId, subscriptionId } = do- DataSyncReady { subscriptions } <- getState- case HashMap.lookup subscriptionId subscriptions of- Just closeSignalMVar -> do- -- Cancel table watcher- MVar.putMVar closeSignalMVar ()-- atomicModifyIORef'' ?state (\state -> state |> modify #subscriptions (HashMap.delete subscriptionId))-- sendJSON DidDeleteDataSubscription { subscriptionId, requestId }- Nothing -> error ("Failed to delete DataSubscription, could not find DataSubscription with id " <> tshow subscriptionId)-- handleMessage CreateRecordMessage { table, record, requestId, transactionId } = do- ensureRLSEnabled table-- let query = "INSERT INTO ? ? VALUES ? RETURNING *"- let columns = record- |> HashMap.keys- |> map (renamer table).fieldToColumn-- let values = record- |> HashMap.elems- |> map aesonValueToPostgresValue-- let params = (PG.Identifier table, PG.In (map PG.Identifier columns), PG.In values)- - result :: [[Field]] <- sqlQueryWithRLSAndTransactionId transactionId query params-- case result of- [rawRecord] ->- let- record = map (renameField (renamer table)) rawRecord- in- sendJSON DidCreateRecord { requestId, record }- otherwise -> error "Unexpected result in CreateRecordMessage handler"-- pure ()- - handleMessage CreateRecordsMessage { table, records, requestId, transactionId } = do- ensureRLSEnabled table-- let query = "INSERT INTO ? ? ? RETURNING *"- let columns = records- |> head- |> \case- Just value -> value- Nothing -> error "Atleast one record is required"- |> HashMap.keys- |> map (renamer table).fieldToColumn-- let values = records- |> map (\object ->- object- |> HashMap.elems- |> map aesonValueToPostgresValue- )- -- let params = (PG.Identifier table, PG.In (map PG.Identifier columns), PG.Values [] values)-- rawRecords :: [[Field]] <- sqlQueryWithRLSAndTransactionId transactionId query params- let records = map (map (renameField (renamer table))) rawRecords-- sendJSON DidCreateRecords { requestId, records }-- pure ()-- handleMessage UpdateRecordMessage { table, id, patch, requestId, transactionId } = do- ensureRLSEnabled table-- let columns = patch- |> HashMap.keys- |> map (renamer table).fieldToColumn- |> map PG.Identifier-- let values = patch- |> HashMap.elems- |> map aesonValueToPostgresValue-- let keyValues = zip columns values-- let setCalls = keyValues- |> map (\_ -> "? = ?")- |> ByteString.intercalate ", "- let query = "UPDATE ? SET " <> setCalls <> " WHERE id = ? RETURNING *"-- let params = [PG.toField (PG.Identifier table)]- <> (join (map (\(key, value) -> [PG.toField key, value]) keyValues))- <> [PG.toField id]-- result :: [[Field]] <- sqlQueryWithRLSAndTransactionId transactionId (PG.Query query) params- - case result of- [rawRecord] ->- let- record = map (renameField (renamer table)) rawRecord- in- sendJSON DidUpdateRecord { requestId, record }- otherwise -> error "Could not apply the update to the given record. Are you sure the record ID you passed is correct? If the record ID is correct, likely the row level security policy is not making the record visible to the UPDATE operation."-- pure ()-- handleMessage UpdateRecordsMessage { table, ids, patch, requestId, transactionId } = do- ensureRLSEnabled table-- let columns = patch- |> HashMap.keys- |> map (renamer table).fieldToColumn- |> map PG.Identifier-- let values = patch- |> HashMap.elems- |> map aesonValueToPostgresValue-- let keyValues = zip columns values-- let setCalls = keyValues- |> map (\_ -> "? = ?")- |> ByteString.intercalate ", "- let query = "UPDATE ? SET " <> setCalls <> " WHERE id IN ? RETURNING *"-- let params = [PG.toField (PG.Identifier table)]- <> (join (map (\(key, value) -> [PG.toField key, value]) keyValues))- <> [PG.toField (PG.In ids)]-- rawRecords <- sqlQueryWithRLSAndTransactionId transactionId (PG.Query query) params- let records = map (map (renameField (renamer table))) rawRecords- - sendJSON DidUpdateRecords { requestId, records }-- pure ()- - handleMessage DeleteRecordMessage { table, id, requestId, transactionId } = do- ensureRLSEnabled table-- sqlExecWithRLSAndTransactionId transactionId "DELETE FROM ? WHERE id = ?" (PG.Identifier table, id)-- sendJSON DidDeleteRecord { requestId }- - handleMessage DeleteRecordsMessage { table, ids, requestId, transactionId } = do- ensureRLSEnabled table-- sqlExecWithRLSAndTransactionId transactionId "DELETE FROM ? WHERE id IN ?" (PG.Identifier table, PG.In ids)-- sendJSON DidDeleteRecords { requestId }-- handleMessage StartTransaction { requestId } = do- ensureBelowTransactionLimit-- transactionId <- UUID.nextRandom--- let takeConnection = ?modelContext.connectionPool- |> Pool.takeResource-- let releaseConnection (connection, localPool) = do- PG.execute connection "ROLLBACK" () -- Make sure there's no pending transaction in case something went wrong- Pool.putResource localPool connection-- Exception.bracket takeConnection releaseConnection \(connection, localPool) -> do- transactionSignal <- MVar.newEmptyMVar-- let globalModelContext = ?modelContext- let ?modelContext = globalModelContext { transactionConnection = Just connection } in sqlExecWithRLS "BEGIN" ()-- let transaction = DataSyncTransaction- { id = transactionId- , connection- , close = transactionSignal- }-- atomicModifyIORef'' ?state (\state -> state |> modify #transactions (HashMap.insert transactionId transaction))-- sendJSON DidStartTransaction { requestId, transactionId }-- MVar.takeMVar transactionSignal-- atomicModifyIORef'' ?state (\state -> state |> modify #transactions (HashMap.delete transactionId))-- handleMessage RollbackTransaction { requestId, id } = do- DataSyncTransaction { id, close } <- findTransactionById id-- sqlExecWithRLSAndTransactionId (Just id) "ROLLBACK" ()- MVar.putMVar close ()-- sendJSON DidRollbackTransaction { requestId, transactionId = id }-- handleMessage CommitTransaction { requestId, id } = do- DataSyncTransaction { id, close } <- findTransactionById id-- sqlExecWithRLSAndTransactionId (Just id) "COMMIT" ()- MVar.putMVar close ()-- sendJSON DidCommitTransaction { requestId, transactionId = id }-- handleMessage otherwise = handleCustomMessage sendJSON otherwise--cleanupAllSubscriptions :: (?state :: IORef DataSyncController, ?applicationContext :: ApplicationContext) => IO ()-cleanupAllSubscriptions = do- state <- getState- let pgListener = ?applicationContext.pgListener-- case state of- DataSyncReady { asyncs } -> forEach asyncs uninterruptibleCancel- _ -> pure ()--changesToValue :: Renamer -> [ChangeNotifications.Change] -> Value-changesToValue renamer changes = object (map changeToPair changes)- where- changeToPair ChangeNotifications.Change { col, new } = (Aeson.fromText $ renamer.columnToField col) .= new--runInModelContextWithTransaction :: (?state :: IORef DataSyncController, ?modelContext :: ModelContext) => ((?modelContext :: ModelContext) => IO result) -> Maybe UUID -> IO result-runInModelContextWithTransaction function (Just transactionId) = do- let globalModelContext = ?modelContext-- DataSyncTransaction { connection } <- findTransactionById transactionId- let- ?modelContext = globalModelContext { transactionConnection = Just connection }- in- function-runInModelContextWithTransaction function Nothing = function--findTransactionById :: (?state :: IORef DataSyncController) => UUID -> IO DataSyncTransaction-findTransactionById transactionId = do- transactions <- (.transactions) <$> readIORef ?state- case HashMap.lookup transactionId transactions of- Just transaction -> pure transaction- Nothing -> error "No transaction with that id"---- | Allow max 10 concurrent transactions per connection to avoid running out of database connections------ Each transaction removes a database connection from the connection pool. If we don't limit the transactions,--- a single user could take down the application by starting more than 'IHP.FrameworkConfig.DBPoolMaxConnections'--- concurrent transactions. Then all database connections are removed from the connection pool and further database--- queries for other users will fail.----ensureBelowTransactionLimit :: (?state :: IORef DataSyncController, ?context :: ControllerContext) => IO ()-ensureBelowTransactionLimit = do- transactions <- (.transactions) <$> readIORef ?state- let transactionCount = HashMap.size transactions- when (transactionCount >= maxTransactionsPerConnection) do- error ("You've reached the transaction limit of " <> tshow maxTransactionsPerConnection <> " transactions")--ensureBelowSubscriptionsLimit :: (?state :: IORef DataSyncController, ?context :: ControllerContext) => IO ()-ensureBelowSubscriptionsLimit = do- subscriptions <- (.subscriptions) <$> readIORef ?state- let subscriptionsCount = HashMap.size subscriptions- when (subscriptionsCount >= maxSubscriptionsPerConnection) do- error ("You've reached the subscriptions limit of " <> tshow maxSubscriptionsPerConnection <> " subscriptions")--maxTransactionsPerConnection :: (?context :: ControllerContext) => Int-maxTransactionsPerConnection = - case getAppConfig @DataSyncMaxTransactionsPerConnection of- DataSyncMaxTransactionsPerConnection value -> value--maxSubscriptionsPerConnection :: (?context :: ControllerContext) => Int-maxSubscriptionsPerConnection = - case getAppConfig @DataSyncMaxSubscriptionsPerConnection of- DataSyncMaxSubscriptionsPerConnection value -> value--sqlQueryWithRLSAndTransactionId ::- ( ?modelContext :: ModelContext- , PG.ToRow parameters- , ?context :: ControllerContext- , userId ~ Id CurrentUserRecord- , Show (PrimaryKey (GetTableName CurrentUserRecord))- , HasNewSessionUrl CurrentUserRecord- , Typeable CurrentUserRecord- , ?context :: ControllerContext- , HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- , PG.ToField userId- , FromRow result- , ?state :: IORef DataSyncController- ) => Maybe UUID -> PG.Query -> parameters -> IO [result]-sqlQueryWithRLSAndTransactionId transactionId theQuery theParams = runInModelContextWithTransaction (sqlQueryWithRLS theQuery theParams) transactionId--sqlExecWithRLSAndTransactionId ::- ( ?modelContext :: ModelContext- , PG.ToRow parameters- , ?context :: ControllerContext- , userId ~ Id CurrentUserRecord- , Show (PrimaryKey (GetTableName CurrentUserRecord))- , HasNewSessionUrl CurrentUserRecord- , Typeable CurrentUserRecord- , ?context :: ControllerContext- , HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- , PG.ToField userId- , ?state :: IORef DataSyncController- ) => Maybe UUID -> PG.Query -> parameters -> IO Int64-sqlExecWithRLSAndTransactionId transactionId theQuery theParams = runInModelContextWithTransaction (sqlExecWithRLS theQuery theParams) transactionId--instance SetField "subscriptions" DataSyncController (HashMap UUID (MVar.MVar ())) where- setField subscriptions record = record { subscriptions }--instance SetField "transactions" DataSyncController (HashMap UUID DataSyncTransaction) where- setField transactions record = record { transactions }--instance SetField "asyncs" DataSyncController [Async ()] where- setField asyncs record = record { asyncs }--atomicModifyIORef'' ref updateFn = atomicModifyIORef' ref (\value -> (updateFn value, ()))
@@ -1,221 +0,0 @@-{-# LANGUAGE DeriveAnyClass #-}-{-|-Module: IHP.DataSync.DynamicQuery-Description: The normal IHP query functionality is type-safe. This module provides type-unsafe access to the database.-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.DataSync.DynamicQuery where--import IHP.ControllerPrelude hiding (OrderByClause)-import Data.Aeson-import qualified Database.PostgreSQL.Simple.FromField as PG-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified IHP.QueryBuilder as QueryBuilder-import Data.Aeson.TH-import qualified GHC.Generics-import qualified Control.DeepSeq as DeepSeq-import qualified Data.Aeson.KeyMap as Aeson-import qualified Data.Aeson.Key as Aeson--data Field = Field { fieldName :: Text, fieldValue :: DynamicValue }--data DynamicValue- = IntValue !Int- | DoubleValue !Double- | TextValue !Text- | BoolValue !Bool- | UUIDValue !UUID- | DateTimeValue !UTCTime- | PointValue !Point- | IntervalValue !PGInterval- | ArrayValue ![DynamicValue]- | Null- deriving (Show, Eq)--newtype UndecodedJSON = UndecodedJSON ByteString- deriving (Show, Eq)---- | Similiar to IHP.QueryBuilder.SQLQuery, but is designed to be accessed by external users------ When compiling to SQL we have to be extra careful to escape all identifers and variables in the query.--- The normal IHP.QueryBuilder doesn't need to be that careful as parts of the input are derived from--- generated code from the Schema.sql.----data DynamicSQLQuery = DynamicSQLQuery- { table :: !Text- , selectedColumns :: SelectedColumns- , whereCondition :: !(Maybe ConditionExpression)- , orderByClause :: ![OrderByClause]- , distinctOnColumn :: !(Maybe ByteString)- , limit :: !(Maybe Int)- , offset :: !(Maybe Int)- } deriving (Show, Eq)--data OrderByClause- = OrderByClause- { orderByColumn :: !ByteString- , orderByDirection :: !OrderByDirection }- | OrderByTSRank { tsvector :: Text, tsquery :: !Text }- deriving (Show, Eq, GHC.Generics.Generic, DeepSeq.NFData)---- | Represents a WHERE conditions of a 'DynamicSQLQuery'-data ConditionExpression- = ColumnExpression { field :: !Text }- | InfixOperatorExpression- { left :: !ConditionExpression- , op :: !ConditionOperator- , right :: !ConditionExpression- }- | LiteralExpression { value :: !DynamicValue }- | CallExpression { functionCall :: !FunctionCall }- | ListExpression { values :: ![DynamicValue] }- deriving (Show, Eq)--data FunctionCall- = ToTSQuery { text :: !Text } -- ^ to_tsquery('english', text)- deriving (Show, Eq, GHC.Generics.Generic, DeepSeq.NFData)---- | Operators available in WHERE conditions-data ConditionOperator- = OpEqual -- ^ a = b- | OpGreaterThan -- ^ a > b- | OpLessThan -- ^ a < b- | OpGreaterThanOrEqual -- ^ a >= b- | OpLessThanOrEqual -- ^ a <= b- | OpNotEqual -- ^ a <> b- | OpAnd -- ^ a AND b- | OpOr -- ^ a OR b- | OpIs -- ^ a IS b- | OpIsNot -- ^ a IS NOT b- | OpTSMatch -- ^ tsvec_a @@ tsvec_b- | OpIn -- ^ a IN b- deriving (Show, Eq)--data SelectedColumns- = SelectAll -- ^ SELECT * FROM table- | SelectSpecific [Text] -- ^ SELECT a, b, c FROM table- deriving (Show, Eq)--instance FromJSON ByteString where- parseJSON (String v) = pure $ cs v--instance FromJSON PG.Action where- parseJSON (String v) = pure (PG.Escape (cs v))--instance {-# OVERLAPS #-} ToJSON [Field] where- toJSON fields = object (map (\Field { fieldName, fieldValue } -> (cs fieldName) .= (toJSON fieldValue)) fields)- toEncoding fields = pairs $ foldl' (<>) mempty encodedFields- where- encodedFields = (map (\Field { fieldName, fieldValue } -> (cs fieldName) .= (toJSON fieldValue)) fields)--instance ToJSON DynamicValue where- toJSON (IntValue value) = toJSON value- toJSON (DoubleValue value) = toJSON value- toJSON (TextValue value) = toJSON value- toJSON (BoolValue value) = toJSON value- toJSON (UUIDValue value) = toJSON value- toJSON (DateTimeValue value) = toJSON value- toJSON (PointValue value) = toJSON value- toJSON (IntervalValue value) = toJSON value- toJSON (ArrayValue value) = toJSON value- toJSON IHP.DataSync.DynamicQuery.Null = toJSON Data.Aeson.Null--instance PG.FromField Field where- fromField field fieldValue' = do- fieldValue <- PG.fromField field fieldValue'- pure Field { .. }- where- fieldName = (PG.name field)- |> fmap cs- |> fromMaybe ""--instance PG.FromField DynamicValue where- fromField field fieldValue' = fieldValue- where- fieldValue =- (IntValue <$> PG.fromField field fieldValue')- <|> (TextValue <$> PG.fromField field fieldValue')- <|> (BoolValue <$> PG.fromField field fieldValue')- <|> (UUIDValue <$> PG.fromField field fieldValue')- <|> (DoubleValue <$> PG.fromField field fieldValue')- <|> (DateTimeValue <$> PG.fromField field fieldValue')- <|> (PointValue <$> PG.fromField field fieldValue')- <|> (IntervalValue <$> PG.fromField field fieldValue')- <|> (ArrayValue <$> PG.fromField field fieldValue')- <|> (PG.fromField @PG.Null field fieldValue' >> pure IHP.DataSync.DynamicQuery.Null)- <|> fromFieldCustomEnum field fieldValue'-- fromFieldCustomEnum field (Just value) = pure (TextValue (cs value))- fromFieldCustomEnum field Nothing = pure IHP.DataSync.DynamicQuery.Null--instance PG.FromField UndecodedJSON where- fromField field (Just value) = pure (UndecodedJSON value)- fromField field Nothing = pure (UndecodedJSON "null")---- | Returns a list of all id's in a result-recordIds :: [[Field]] -> [UUID]-recordIds result = result- |> concat- |> filter (\Field { fieldName } -> fieldName == "id")- |> map (.fieldValue)- |> mapMaybe \case- UUIDValue uuid -> Just uuid- otherwise -> Nothing------ Here you can add functions which are available in all your controllers---- | Transforms the keys of a JSON object from field name to column name------ >>> transformColumnNamesToFieldNames [json|{"isCompleted": true}|]--- [json|{"is_completed": true}|]-transformColumnNamesToFieldNames :: Value -> Value-transformColumnNamesToFieldNames (Object hashMap) =- hashMap- |> Aeson.toList- |> map (\(key, value) -> (applyKey columnNameToFieldName key, value))- |> Aeson.fromList- |> Object- where- applyKey function key =- key- |> Aeson.toText- |> function- |> Aeson.fromText---$(deriveFromJSON defaultOptions ''FunctionCall)-$(deriveFromJSON defaultOptions 'QueryBuilder.OrCondition)-$(deriveFromJSON defaultOptions 'QueryBuilder.Join)-$(deriveFromJSON defaultOptions ''QueryBuilder.OrderByDirection)-$(deriveFromJSON defaultOptions 'QueryBuilder.OrderByClause)-$(deriveFromJSON defaultOptions 'SelectAll)-$(deriveFromJSON defaultOptions ''ConditionOperator)-$(deriveFromJSON defaultOptions ''DynamicValue)-$(deriveFromJSON defaultOptions ''ConditionExpression)--instance FromJSON DynamicSQLQuery where- parseJSON = withObject "DynamicSQLQuery" $ \v -> DynamicSQLQuery- <$> v .: "table"- <*> v .: "selectedColumns"- <*> v .: "whereCondition"- <*> v .: "orderByClause"- <*> v .:? "distinctOnColumn" -- distinctOnColumn can be absent in older versions of ihp-datasync.js- <*> v .:? "limit" -- Limit can be absent in older versions of ihp-datasync.js- <*> v .:? "offset" -- Offset can be absent in older versions of ihp-datasync.js---instance FromJSON OrderByClause where- parseJSON = withObject "OrderByClause" $ \v -> do- let oldFormat = OrderByClause- <$> v .: "orderByColumn"- <*> v .: "orderByDirection"- let tagged = do- tag <- v .: "tag"- case tag of- "OrderByClause" -> oldFormat- "OrderByTSRank" -> OrderByTSRank <$> v .: "tsvector" <*> v .: "tsquery"- otherwise -> error ("Invalid tag: " <> otherwise)- tagged <|> oldFormat
@@ -1,190 +0,0 @@-{-|-Module: IHP.DataSync.DynamicQueryCompiler-Description: Compiles a DynamicQuery to SQL-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.DataSync.DynamicQueryCompiler where--import IHP.Prelude-import IHP.DataSync.DynamicQuery-import qualified IHP.QueryBuilder as QueryBuilder-import qualified Database.PostgreSQL.Simple as PG-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Data.List as List--data Renamer = Renamer- { fieldToColumn :: Text -> Text- , columnToField :: Text -> Text- }--compileQuery :: DynamicSQLQuery -> (PG.Query, [PG.Action])-compileQuery = compileQueryWithRenamer camelCaseRenamer--compileQueryWithRenamer :: Renamer -> DynamicSQLQuery -> (PG.Query, [PG.Action])-compileQueryWithRenamer renamer query = compileQueryMapped (mapColumnNames renamer.fieldToColumn query)---- | Default renamer used by DataSync.------ Transforms JS inputs in @camelCase@ to snake_case for the database--- and DB outputs in @snake_case@ back to @camelCase@-camelCaseRenamer :: Renamer-camelCaseRenamer =- Renamer- { fieldToColumn = fieldNameToColumnName- , columnToField = columnNameToFieldName- }---- | Renamer that does not modify the column names-unmodifiedRenamer :: Renamer-unmodifiedRenamer =- Renamer- { fieldToColumn = id- , columnToField = id- }---- | When a Field is retrieved from the database, it's all in @snake_case@. This turns it into @camelCase@-renameField :: Renamer -> Field -> Field-renameField renamer field =- field { fieldName = renamer.columnToField field.fieldName }--compileQueryMapped :: DynamicSQLQuery -> (PG.Query, [PG.Action])-compileQueryMapped DynamicSQLQuery { .. } = (sql, args)- where- sql = "SELECT" <> distinctOnSql <> "? FROM ?" <> whereSql <> orderBySql <> limitSql <> offsetSql- args = distinctOnArgs- <> catMaybes- [ Just (compileSelectedColumns selectedColumns)- , Just (PG.toField (PG.Identifier table))- ]- <> whereArgs- <> orderByArgs- <> limitArgs- <> offsetArgs-- (distinctOnSql, distinctOnArgs) = case distinctOnColumn of- Just column -> (" DISTINCT ON (?) ", [PG.toField $ PG.Identifier (cs column)])- Nothing -> (" ", [])-- (orderBySql, orderByArgs) = case orderByClause of- [] -> ("", [])- orderByClauses ->- ( PG.Query $ cs $ " ORDER BY " <> (intercalate ", " (map compileOrderByClause orderByClauses))- , orderByClauses- |> map (\case- OrderByClause { orderByColumn, orderByDirection } ->- [ PG.toField $ PG.Identifier (cs orderByColumn)- , PG.toField $ if orderByDirection == QueryBuilder.Desc- then PG.Plain "DESC"- else PG.Plain ""- ]- OrderByTSRank { tsvector, tsquery } ->- [ PG.toField $ PG.Identifier tsvector- , PG.toField tsquery- ]- )- |> concat- )-- (whereSql, whereArgs) = case compileCondition <$> whereCondition of- Just (sql, args) -> (" WHERE " <> sql, args)- Nothing -> ("", [])-- (limitSql, limitArgs) = case limit of- Just limit -> (" LIMIT ?", [PG.toField limit])- Nothing -> ("", [])-- (offsetSql, offsetArgs) = case offset of- Just offset -> (" OFFSET ?", [PG.toField offset])- Nothing -> ("", [])---- | Used to transform column names from @camelCase@ to @snake_case@-mapColumnNames :: (Text -> Text) -> DynamicSQLQuery -> DynamicSQLQuery-mapColumnNames rename query =- query- { selectedColumns = mapSelectedColumns query.selectedColumns- , whereCondition = mapConditionExpression <$> query.whereCondition- , orderByClause = map mapOrderByClause query.orderByClause- , distinctOnColumn = (cs . rename . cs) <$> query.distinctOnColumn- }- where- mapSelectedColumns :: SelectedColumns -> SelectedColumns- mapSelectedColumns SelectAll = SelectAll- mapSelectedColumns (SelectSpecific columns) = SelectSpecific (map rename columns)-- mapConditionExpression :: ConditionExpression -> ConditionExpression- mapConditionExpression ColumnExpression { field } = ColumnExpression { field = rename field }- mapConditionExpression InfixOperatorExpression { left, op, right } = InfixOperatorExpression { left = mapConditionExpression left, op, right = mapConditionExpression right }- mapConditionExpression otherwise = otherwise-- mapOrderByClause :: OrderByClause -> OrderByClause- mapOrderByClause OrderByClause { orderByColumn, orderByDirection } = OrderByClause { orderByColumn = cs (rename (cs orderByColumn)), orderByDirection }- mapOrderByClause otherwise = otherwise--compileOrderByClause :: OrderByClause -> Text-compileOrderByClause OrderByClause {} = "? ?"-compileOrderByClause OrderByTSRank { tsvector, tsquery } = "ts_rank(?, to_tsquery('english', ?))"--compileSelectedColumns :: SelectedColumns -> PG.Action-compileSelectedColumns SelectAll = PG.Plain "*"-compileSelectedColumns (SelectSpecific fields) = PG.Many args- where- args :: [PG.Action]- args = List.intercalate ([PG.Plain ", "]) fieldActions- fieldActions :: [[PG.Action]]- fieldActions = (map (\field -> [ PG.toField (PG.Identifier field) ]) fields)---- TODO: validate query against schema--compileCondition :: ConditionExpression -> (PG.Query, [PG.Action])-compileCondition (ColumnExpression column) = ("?", [PG.toField $ PG.Identifier column])-compileCondition (InfixOperatorExpression a OpEqual (LiteralExpression Null)) = compileCondition (InfixOperatorExpression a OpIs (LiteralExpression Null)) -- Turn 'a = NULL' into 'a IS NULL'-compileCondition (InfixOperatorExpression a OpNotEqual (LiteralExpression Null)) = compileCondition (InfixOperatorExpression a OpIsNot (LiteralExpression Null)) -- Turn 'a <> NULL' into 'a IS NOT NULL'-compileCondition (InfixOperatorExpression a OpIn (ListExpression { values })) | (Null `List.elem` values) =- -- Turn 'a IN (NULL)' into 'a IS NULL'- case partition ((/=) Null) values of- ([], nullValues) -> compileCondition (InfixOperatorExpression a OpIs (LiteralExpression Null))- (nonNullValues, nullValues) -> compileCondition (InfixOperatorExpression (InfixOperatorExpression a OpIn (ListExpression { values = nonNullValues })) OpOr (InfixOperatorExpression a OpIs (LiteralExpression Null)))-compileCondition (InfixOperatorExpression a operator b) = ("(" <> queryA <> ") " <> compileOperator operator <> " " <> rightOperand, paramsA <> paramsB)- where- (queryA, paramsA) = compileCondition a- (queryB, paramsB) = compileCondition b-- rightOperand = if rightParentheses- then "(" <> queryB <> ")"- else queryB-- rightParentheses :: Bool- rightParentheses =- case b of- LiteralExpression Null -> False- ListExpression {} -> False -- The () are inserted already via @PG.In@- _ -> True-compileCondition (LiteralExpression literal) = ("?", [PG.toField literal])-compileCondition (CallExpression { functionCall = ToTSQuery { text } }) = ("to_tsquery('english', ?)", [PG.toField text])-compileCondition (ListExpression { values }) = ("?", [PG.toField (PG.In values)])--compileOperator :: ConditionOperator -> PG.Query-compileOperator OpEqual = "="-compileOperator OpGreaterThan = ">"-compileOperator OpLessThan = "<"-compileOperator OpGreaterThanOrEqual = ">="-compileOperator OpLessThanOrEqual = "<="-compileOperator OpNotEqual = "<>"-compileOperator OpAnd = "AND"-compileOperator OpOr = "OR"-compileOperator OpIs = "IS"-compileOperator OpIsNot = "IS NOT"-compileOperator OpTSMatch = "@@"-compileOperator OpIn = "IN"--instance PG.ToField DynamicValue where- toField (IntValue int) = PG.toField int- toField (DoubleValue double) = PG.toField double- toField (TextValue text) = PG.toField text- toField (BoolValue bool) = PG.toField bool- toField (UUIDValue uuid) = PG.toField uuid- toField (DateTimeValue utcTime) = PG.toField utcTime- toField (PointValue point) = PG.toField point- toField (ArrayValue values) = PG.toField (PG.PGArray values)- toField Null = PG.toField PG.Null
@@ -1,239 +0,0 @@-{-# LANGUAGE UndecidableInstances #-}-module IHP.DataSync.REST.Controller where--import IHP.ControllerPrelude hiding (OrderByClause)-import IHP.DataSync.REST.Types-import Data.Aeson-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Database.PostgreSQL.Simple as PG-import qualified Data.Vector as Vector-import qualified Data.ByteString.Char8 as ByteString-import qualified Control.Exception.Safe as Exception-import IHP.DataSync.RowLevelSecurity-import IHP.DataSync.DynamicQuery-import IHP.DataSync.Types-import Network.HTTP.Types (status400)-import IHP.DataSync.DynamicQueryCompiler-import qualified Data.Text as Text-import qualified Data.Scientific as Scientific--import qualified Data.ByteString.Builder as ByteString-import qualified Data.Aeson.Encoding.Internal as Aeson-import qualified Data.Aeson.KeyMap as Aeson-import qualified Data.Aeson.Key as Aeson---instance (- PG.ToField (PrimaryKey (GetTableName CurrentUserRecord))- , Show (PrimaryKey (GetTableName CurrentUserRecord))- , HasNewSessionUrl CurrentUserRecord- , Typeable CurrentUserRecord- , HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- ) => Controller ApiController where- action CreateRecordAction { table } = do- ensureRLSEnabled table-- let payload = requestBodyJSON-- case payload of- Object hashMap -> do- let query = "INSERT INTO ? ? VALUES ? RETURNING *"- let columns = hashMap- |> Aeson.keys- |> map (fieldNameToColumnName . Aeson.toText)-- let values = hashMap- |> Aeson.elems- |> map aesonValueToPostgresValue-- let params = (PG.Identifier table, PG.In (map PG.Identifier columns), PG.In values)-- result :: Either EnhancedSqlError [[Field]] <- Exception.try do- sqlQueryWithRLS query params-- case result of- Left error -> renderErrorJson error- Right result -> renderJson result-- Array objects -> do- let query = "INSERT INTO ? ? ? RETURNING *"- let columns = objects- |> Vector.toList- |> head- |> \case- Just value -> value- Nothing -> error "Atleast one record is required"- |> \case- Object hashMap -> hashMap- otherwise -> error "Expected object"- |> Aeson.keys- |> map (fieldNameToColumnName . Aeson.toText)-- let values = objects- |> Vector.toList- |> map (\object ->- object- |> \case- Object hashMap -> hashMap- otherwise -> error "Expected object"- |> Aeson.elems- |> map aesonValueToPostgresValue- )--- let params = (PG.Identifier table, PG.In (map PG.Identifier columns), PG.Values [] values)-- result :: [[Field]] <- sqlQueryWithRLS query params- renderJson result---- action UpdateRecordAction { table, id } = do- ensureRLSEnabled table-- let payload = requestBodyJSON- |> \case- Object hashMap -> hashMap-- let columns = payload- |> Aeson.keys- |> map (fieldNameToColumnName . Aeson.toText)- |> map PG.Identifier-- let values = payload- |> Aeson.elems- |> map aesonValueToPostgresValue-- let keyValues = zip columns values-- let setCalls = keyValues- |> map (\_ -> "? = ?")- |> ByteString.intercalate ", "- let query = "UPDATE ? SET " <> setCalls <> " WHERE id = ? RETURNING *"-- let params = [PG.toField (PG.Identifier table)]- <> (join (map (\(key, value) -> [PG.toField key, value]) keyValues))- <> [PG.toField id]-- result :: [[Field]] <- sqlQueryWithRLS (PG.Query query) params-- renderJson (head result)-- -- DELETE /api/:table/:id- action DeleteRecordAction { table, id } = do- ensureRLSEnabled table-- sqlExecWithRLS "DELETE FROM ? WHERE id = ?" (PG.Identifier table, id)-- renderJson True-- -- GET /api/:table/:id- action ShowRecordAction { table, id } = do- ensureRLSEnabled table-- result :: [[Field]] <- sqlQueryWithRLS "SELECT * FROM ? WHERE id = ?" (PG.Identifier table, id)-- renderJson (head result)-- -- GET /api/:table- -- GET /api/:table?orderBy=createdAt- -- GET /api/:table?fields=id,title- action ListRecordsAction { table } = do- ensureRLSEnabled table-- let (theQuery, theParams) = compileQuery (buildDynamicQueryFromRequest table)- result :: [[Field]] <- sqlQueryWithRLS theQuery theParams-- renderJson result--buildDynamicQueryFromRequest table = DynamicSQLQuery- { table- , selectedColumns = paramOrDefault SelectAll "fields"- , whereCondition = Nothing- , orderByClause = paramList "orderBy"- , distinctOnColumn = paramOrNothing "distinctOnColumn"- , limit = paramOrNothing "limit"- , offset = paramOrNothing "offset"- }--instance ParamReader SelectedColumns where- readParameter byteString = pure $- byteString- |> cs- |> Text.split (\char -> char == ',')- |> SelectSpecific--instance ParamReader OrderByClause where- readParameter byteString = case ByteString.split ',' byteString of- [orderByColumn, order] -> do- orderByDirection <- parseOrder order- pure OrderByClause { orderByColumn, orderByDirection }- [orderByColumn] -> pure OrderByClause { orderByColumn, orderByDirection = Asc }- where- parseOrder "asc" = Right Asc- parseOrder "desc" = Right Desc- parseOrder otherwise = Left ("Invalid order " <> cs otherwise)--instance ToJSON PG.SqlError where- toJSON PG.SqlError { sqlState, sqlErrorMsg, sqlErrorDetail, sqlErrorHint } = object- [ "state" .= ((cs sqlState) :: Text)- , "errorMsg" .= ((cs sqlErrorMsg) :: Text)- , "errorDetail" .= ((cs sqlErrorDetail) :: Text)- , "errorHint" .= ((cs sqlErrorHint) :: Text)- ]- where- fieldValueToJSON (IntValue value) = toJSON value- fieldValueToJSON (TextValue value) = toJSON value- fieldValueToJSON (BoolValue value) = toJSON value- fieldValueToJSON (UUIDValue value) = toJSON value- fieldValueToJSON (DateTimeValue value) = toJSON value--instance ToJSON EnhancedSqlError where- toJSON EnhancedSqlError { sqlError } = toJSON sqlError--renderErrorJson :: (?context :: ControllerContext) => Data.Aeson.ToJSON json => json -> IO ()-renderErrorJson json = renderJsonWithStatusCode status400 json-{-# INLINABLE renderErrorJson #-}--aesonValueToPostgresValue :: Value -> PG.Action-aesonValueToPostgresValue (String text) = PG.toField text-aesonValueToPostgresValue (Bool value) = PG.toField value-aesonValueToPostgresValue (Number value) = case Scientific.floatingOrInteger value of -- Hacky, we should make this function "Schema.sql"-aware in the future- Left (floating :: Double) -> PG.toField floating- Right (integer :: Integer) -> PG.toField integer-aesonValueToPostgresValue Data.Aeson.Null = PG.toField PG.Null-aesonValueToPostgresValue (Data.Aeson.Array values) = PG.toField (PG.PGArray (map aesonValueToPostgresValue (Vector.toList values)))-aesonValueToPostgresValue object@(Object values) =- let- tryDecodeAsPoint :: Maybe Point- tryDecodeAsPoint = do- xValue <- Aeson.lookup "x" values- yValue <- Aeson.lookup "y" values- x <- case xValue of- Number number -> pure (Scientific.toRealFloat number)- otherwise -> Nothing- y <- case yValue of- Number number -> pure (Scientific.toRealFloat number)- otherwise -> Nothing- pure Point { x, y }- in- -- This is really hacky and is mostly duck typing. We should refactor this in the future to- -- become more type aware by passing the DDL of the table to 'aesonValueToPostgresValue'.- if Aeson.size values == 2- then fromMaybe (PG.toField $ toJSON object) (PG.toField <$> tryDecodeAsPoint)- else PG.toField (toJSON object)---instance ToJSON GraphQLResult where- toJSON GraphQLResult { requestId, graphQLResult } = object [ "tag" .= ("GraphQLResult" :: Text), "requestId" .= requestId, "graphQLResult" .= ("" :: Text) ]- toEncoding GraphQLResult { requestId, graphQLResult } = Aeson.econcat- [ Aeson.unsafeToEncoding "{\"tag\":\"GraphQLResult\",\"requestId\":"- , Aeson.int requestId- , Aeson.unsafeToEncoding ",\"graphQLResult\":"- , toEncoding graphQLResult- , Aeson.unsafeToEncoding "}"- ]-instance ToJSON UndecodedJSON where- toJSON (UndecodedJSON _) = error "Not implemented"- toEncoding (UndecodedJSON json) = Aeson.unsafeToEncoding (ByteString.byteString json)
@@ -1,50 +0,0 @@-module IHP.DataSync.REST.Routes where--import IHP.RouterPrelude-import IHP.DataSync.REST.Types--instance CanRoute ApiController where- parseRoute' = do- string "/api/"-- let- graphQLQueryAction = do- string "graphql"- endOfInput- onlyAllowMethods [POST]- pure GraphQLQueryAction-- createRecordAction table = do- endOfInput- onlyAllowMethods [POST]- pure CreateRecordAction { table }-- updateOrDeleteRecordAction table = do- string "/"- id <- parseUUID-- endOfInput-- method <- getMethod- case method of- PATCH -> pure UpdateRecordAction { table, id }- GET -> pure ShowRecordAction { table, id }- DELETE -> pure DeleteRecordAction { table, id }-- listRecordsAction table = do- endOfInput- method <- getMethod- case method of- GET -> pure ListRecordsAction { table }-- crud = do- table <- parseText- updateOrDeleteRecordAction table <|> createRecordAction table <|> listRecordsAction table-- graphQLQueryAction <|> crud--instance HasPath ApiController where- pathTo CreateRecordAction { table } = "/api/" <> table- pathTo UpdateRecordAction { table, id } = "/api/" <> table <> "/" <> tshow id- pathTo DeleteRecordAction { table, id } = "/api/" <> table <> "/" <> tshow id- pathTo GraphQLQueryAction = "/api/graphql"
@@ -1,12 +0,0 @@-module IHP.DataSync.REST.Types where--import IHP.Prelude--data ApiController- = CreateRecordAction { table :: Text } -- ^ POST /api/books- | UpdateRecordAction { table :: Text, id :: UUID }- | DeleteRecordAction { table :: Text, id :: UUID }- | ShowRecordAction { table :: Text, id :: UUID } -- ^ GET /api/books/9ba0ffbc-bfc1-4d7d-8152-30a6648806f7- | ListRecordsAction { table :: Text } -- ^ GET /api/books- | GraphQLQueryAction- deriving (Eq, Show, Data)
@@ -1,69 +0,0 @@-{-|-Module: IHP.DataSync.Role-Description: Postgres role management for RLS-Copyright: (c) digitally induced GmbH, 2021--The default user that creates a table in postgres always-has access to all rows inside the table. The default user is not restricted-to the RLS policies.--Therefore we need to use a second role whenever we want to-make a query with RLS enabled. Basically for every query we do, we'll-wrap it in a transaction and then use 'SET LOCAL ROLE ..' to switch to-our second role for the duration of the transaction.---}-module IHP.DataSync.Role where--import IHP.Prelude-import IHP.FrameworkConfig-import IHP.ModelSupport-import qualified Database.PostgreSQL.Simple.Types as PG--doesRoleExists :: (?modelContext :: ModelContext) => Text -> IO Bool-doesRoleExists name = sqlQueryScalar "SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = ? LIMIT 1)" [name]--ensureAuthenticatedRoleExists :: (?context :: context, ConfigProvider context, ?modelContext :: ModelContext) => IO ()-ensureAuthenticatedRoleExists = do- roleExists <- doesRoleExists authenticatedRole- unless roleExists (createAuthenticatedRole authenticatedRole)- grantPermissions authenticatedRole--createAuthenticatedRole :: (?modelContext :: ModelContext) => Text -> IO ()-createAuthenticatedRole role = do- -- The role is only going to be used from 'SET ROLE ..' calls- -- Therefore we can disallow direct connection with NOLOGIN- sqlExec "CREATE ROLE ? NOLOGIN" [PG.Identifier role]--- pure ()--grantPermissions :: (?modelContext :: ModelContext) => Text -> IO ()-grantPermissions role = do- -- From SO https://stackoverflow.com/a/17355059/14144232- --- -- GRANTs on different objects are separate. GRANTing on a database doesn't GRANT rights to the schema within. Similiarly, GRANTing on a schema doesn't grant rights on the tables within.- -- - -- If you have rights to SELECT from a table, but not the right to see it in the schema that contains it then you can't access the table.- -- - -- The rights tests are done in order:- -- - -- Do you have `USAGE` on the schema? - -- No: Reject access. - -- Yes: Do you also have the appropriate rights on the table? - -- No: Reject access. - -- Yes: Check column privileges.-- -- The role should have access to all existing tables in our schema- sqlExec "GRANT USAGE ON SCHEMA public TO ?" [PG.Identifier role]- - -- The role should have access to all existing tables in our schema- sqlExec "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ?" [PG.Identifier role]-- -- Also grant access to all tables created in the future- sqlExec "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO ?" [PG.Identifier role]-- pure ()--authenticatedRole :: (?context :: context, ConfigProvider context) => Text-authenticatedRole = ?context.frameworkConfig.rlsAuthenticatedRole
@@ -1,138 +0,0 @@-module IHP.DataSync.RowLevelSecurity-( ensureRLSEnabled-, hasRLSEnabled-, TableWithRLS (tableName)-, makeCachedEnsureRLSEnabled-, sqlQueryWithRLS-, sqlExecWithRLS-)-where--import IHP.ControllerPrelude-import qualified Database.PostgreSQL.Simple as PG-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Database.PostgreSQL.Simple.ToRow as PG-import qualified IHP.DataSync.Role as Role-import qualified Data.Set as Set--sqlQueryWithRLS ::- ( ?modelContext :: ModelContext- , PG.ToRow parameters- , ?context :: ControllerContext- , userId ~ Id CurrentUserRecord- , Show (PrimaryKey (GetTableName CurrentUserRecord))- , HasNewSessionUrl CurrentUserRecord- , Typeable CurrentUserRecord- , ?context :: ControllerContext- , HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- , PG.ToField userId- , FromRow result- ) => PG.Query -> parameters -> IO [result]-sqlQueryWithRLS query parameters = sqlQuery queryWithRLS parametersWithRLS- where- (queryWithRLS, parametersWithRLS) = wrapStatementWithRLS query parameters-{-# INLINE sqlQueryWithRLS #-}--sqlExecWithRLS ::- ( ?modelContext :: ModelContext- , PG.ToRow parameters- , ?context :: ControllerContext- , userId ~ Id CurrentUserRecord- , Show (PrimaryKey (GetTableName CurrentUserRecord))- , HasNewSessionUrl CurrentUserRecord- , Typeable CurrentUserRecord- , ?context :: ControllerContext- , HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- , PG.ToField userId- ) => PG.Query -> parameters -> IO Int64-sqlExecWithRLS query parameters = sqlExec queryWithRLS parametersWithRLS- where- (queryWithRLS, parametersWithRLS) = wrapStatementWithRLS query parameters-{-# INLINE sqlExecWithRLS #-}--wrapStatementWithRLS ::- ( ?modelContext :: ModelContext- , PG.ToRow parameters- , ?context :: ControllerContext- , userId ~ Id CurrentUserRecord- , Show (PrimaryKey (GetTableName CurrentUserRecord))- , HasNewSessionUrl CurrentUserRecord- , Typeable CurrentUserRecord- , ?context :: ControllerContext- , HasField "id" CurrentUserRecord (Id' (GetTableName CurrentUserRecord))- , PG.ToField userId- ) => PG.Query -> parameters -> (PG.Query, [PG.Action])-wrapStatementWithRLS query parameters = (queryWithRLS, parametersWithRLS)- where- queryWithRLS = "SET LOCAL ROLE ?; SET LOCAL rls.ihp_user_id = ?; " <> query <> ";"-- maybeUserId = (.id) <$> currentUserOrNothing-- -- When the user is not logged in and maybeUserId is Nothing, we cannot- -- just pass @NULL@ to postgres. The @SET LOCAL@ values can only be strings.- --- -- Therefore we map Nothing to an empty string here. The empty string- -- means "not logged in".- --- encodedUserId = case maybeUserId of- Just userId -> PG.toField userId- Nothing -> PG.toField ("" :: Text)-- parametersWithRLS = [PG.toField (PG.Identifier Role.authenticatedRole), PG.toField encodedUserId] <> (PG.toRow parameters)-{-# INLINE wrapStatementWithRLS #-}---- | Returns a proof that RLS is enabled for a table-ensureRLSEnabled :: (?modelContext :: ModelContext) => Text -> IO TableWithRLS-ensureRLSEnabled table = do- rlsEnabled <- hasRLSEnabled table- unless rlsEnabled (error "Row level security is required for accessing this table")- pure (TableWithRLS table)---- | Returns a factory for 'ensureRLSEnabled' that memoizes when a table has RLS enabled.------ When a table doesn't have RLS enabled yet, the result is not memoized.------ __Example:__------ > -- Setup--- > ensureRLSEnabled <- makeCachedEnsureRLSEnabled--- >--- > ensureRLSEnabled "projects" -- Runs a database query to check if row level security is enabled for the projects table--- >--- > -- Asuming 'ensureRLSEnabled "projects"' proceeded without errors:--- >--- > ensureRLSEnabled "projects" -- Now this will instantly return True and don't fire any SQL queries anymore----makeCachedEnsureRLSEnabled :: (?modelContext :: ModelContext) => IO (Text -> IO TableWithRLS)-makeCachedEnsureRLSEnabled = do- tables <- newIORef Set.empty- pure \tableName -> do- rlsEnabled <- Set.member tableName <$> readIORef tables-- if rlsEnabled- then pure TableWithRLS { tableName }- else do- proof <- ensureRLSEnabled tableName- modifyIORef' tables (Set.insert tableName)- pure proof---- | Returns 'True' if row level security has been enabled on a table------ RLS can be enabled with this SQL statement:------ > ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;------ After this 'hasRLSEnabled' will return true:------ >>> hasRLSEnabled "my_table"--- True-hasRLSEnabled :: (?modelContext :: ModelContext) => Text -> IO Bool-hasRLSEnabled table = sqlQueryScalar "SELECT relrowsecurity FROM pg_class WHERE oid = quote_ident(?)::regclass" [table]---- | Can be constructed using 'ensureRLSEnabled'------ > tableWithRLS <- ensureRLSEnabled "my_table"------ Useful to carry a proof that the RLS is actually enabled-newtype TableWithRLS = TableWithRLS { tableName :: Text } deriving (Eq, Ord)
@@ -1,77 +0,0 @@-module IHP.DataSync.Types where--import IHP.Prelude-import Data.Aeson-import IHP.DataSync.DynamicQuery-import qualified Database.PostgreSQL.Simple as PG-import Control.Concurrent.MVar as MVar---data DataSyncMessage- = DataSyncQuery { query :: !DynamicSQLQuery, requestId :: !Int, transactionId :: !(Maybe UUID) }- | CreateDataSubscription { query :: !DynamicSQLQuery, requestId :: !Int }- | CreateCountSubscription { query :: !DynamicSQLQuery, requestId :: !Int }- | DeleteDataSubscription { subscriptionId :: !UUID, requestId :: !Int }- | CreateRecordMessage { table :: !Text, record :: !(HashMap Text Value), requestId :: !Int, transactionId :: !(Maybe UUID) }- | CreateRecordsMessage { table :: !Text, records :: ![HashMap Text Value], requestId :: !Int, transactionId :: !(Maybe UUID) }- | UpdateRecordMessage { table :: !Text, id :: !UUID, patch :: !(HashMap Text Value), requestId :: !Int, transactionId :: !(Maybe UUID) }- | UpdateRecordsMessage { table :: !Text, ids :: ![UUID], patch :: !(HashMap Text Value), requestId :: !Int, transactionId :: !(Maybe UUID) }- | DeleteRecordMessage { table :: !Text, id :: !UUID, requestId :: !Int, transactionId :: !(Maybe UUID) }- | DeleteRecordsMessage { table :: !Text, ids :: ![UUID], requestId :: !Int, transactionId :: !(Maybe UUID) }- | StartTransaction { requestId :: !Int }- | RollbackTransaction { requestId :: !Int, id :: !UUID }- | CommitTransaction { requestId :: !Int, id :: !UUID }- | LoginWithEmailAndPassword { requestId :: !Int, email :: !Text, password :: !Text }- | LoginWithJWT { requestId :: !Int, jwt :: !Text }- | CreateUser { requestId :: !Int, email :: !Text, password :: !Text }- | ConfirmUser { requestId :: !Int, userId :: !UUID, token :: !Text }- deriving (Eq, Show)--data DataSyncResponse- = DataSyncResult { result :: ![[Field]], requestId :: !Int }- | DataSyncError { requestId :: !Int, errorMessage :: !Text }- | FailedToDecodeMessageError { errorMessage :: !Text }- | DidCreateDataSubscription { requestId :: !Int, subscriptionId :: !UUID, result :: ![[Field]] }- | DidCreateCountSubscription { requestId :: !Int, subscriptionId :: !UUID, count :: !Int }- | DidDeleteDataSubscription { requestId :: !Int, subscriptionId :: !UUID }- | DidInsert { subscriptionId :: !UUID, record :: ![Field] }- | DidUpdate { subscriptionId :: !UUID, id :: UUID, changeSet :: !Value }- | DidDelete { subscriptionId :: !UUID, id :: !UUID }- | DidChangeCount { subscriptionId :: !UUID, count :: !Int }- | DidCreateRecord { requestId :: !Int, record :: ![Field] } -- ^ Response to 'CreateRecordMessage'- | DidCreateRecords { requestId :: !Int, records :: ![[Field]] } -- ^ Response to 'CreateRecordsMessage'- | DidUpdateRecord { requestId :: !Int, record :: ![Field] } -- ^ Response to 'UpdateRecordMessage'- | DidUpdateRecords { requestId :: !Int, records :: ![[Field]] } -- ^ Response to 'UpdateRecordsMessage'- | DidDeleteRecord { requestId :: !Int }- | DidDeleteRecords { requestId :: !Int }- | DidStartTransaction { requestId :: !Int, transactionId :: !UUID }- | DidRollbackTransaction { requestId :: !Int, transactionId :: !UUID }- | DidCommitTransaction { requestId :: !Int, transactionId :: !UUID }-- | LoginSuccessful { requestId :: !Int, userId :: !UUID, jwt :: !Text }- | UserLocked { requestId :: !Int }- | UserUnconfirmed { requestId :: !Int }- | InvalidCredentials { requestId :: !Int }-- | DidCreateUser { requestId :: !Int, userId :: !UUID, emailConfirmationRequired :: !Bool, jwt :: !Text }- | CreateUserFailed { requestId :: !Int, validationFailures :: [(Text, Text)] }- | DidConfirmUser { requestId :: !Int, jwt :: !Text }- | DidConfirmUserAlready { requestId :: !Int }- | ConfirmUserFailed { requestId :: !Int }--data GraphQLResult = GraphQLResult { graphQLResult :: !UndecodedJSON, requestId :: !Int }--data DataSyncTransaction- = DataSyncTransaction- { id :: !UUID- , connection :: !PG.Connection- , close :: MVar ()- }--data DataSyncController- = DataSyncController- | DataSyncReady- { subscriptions :: !(HashMap UUID (MVar.MVar ()))- , transactions :: !(HashMap UUID DataSyncTransaction)- , asyncs :: ![Async ()]- }
@@ -7,11 +7,15 @@ ) where -import IHP.Prelude-import Data.String.Interpolate.IsString (i)+import Prelude+import Data.ByteString (ByteString)+import Data.Text (Text)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Maybe (isJust, fromMaybe)+import Data.String.Conversions (cs)+import IHP.HaskellSupport (textToInt) import qualified System.Posix.Env.ByteString as Posix import Network.Socket (PortNumber)-import IHP.Mail.Types import IHP.Environment -- | Returns a env variable. The raw string@@ -44,7 +48,7 @@ -- "Env var 'PORT' not set, but it's required for the app to run" -- env :: forall result monad. (MonadIO monad) => EnvVarReader result => ByteString -> monad result-env name = envOrDefault name (error [i|Env var '#{name}' not set, but it's required for the app to run|])+env name = envOrDefault name (error ("Env var '" <> cs name <> "' not set, but it's required for the app to run")) {-# INLINE env #-} envOrDefault :: (MonadIO monad) => EnvVarReader result => ByteString -> result -> monad result@@ -55,7 +59,7 @@ envOrNothing name = liftIO $ fmap parseString <$> Posix.getEnv name where parseString string = case envStringToValue string of- Left errorMessage -> error [i|Env var '#{name}' is invalid: #{errorMessage}|]+ Left errorMessage -> error ("Env var '" <> cs name <> "' is invalid: " <> cs errorMessage) Right value -> value {-# INLINE envOrNothing #-} @@ -76,7 +80,7 @@ instance EnvVarReader Int where envStringToValue string = case textToInt (cs string) of Just integer -> Right integer- Nothing -> Left [i|Expected integer, got #{string}|]+ Nothing -> Left ("Expected integer, got " <> cs string) instance EnvVarReader Text where envStringToValue string = Right (cs string)@@ -96,14 +100,7 @@ instance EnvVarReader PortNumber where envStringToValue string = case textToInt (cs string) of Just integer -> Right $ convertIntToPortNumber integer- Nothing -> Left [i|Expected integer to be used as a Port number, got #{string}|]+ Nothing -> Left ("Expected integer to be used as a Port number, got " <> cs string) convertIntToPortNumber :: Int -> PortNumber convertIntToPortNumber int = fromIntegral (int :: Int) :: PortNumber---- | Allow reading the env var of an SMTP encryption method.-instance EnvVarReader SMTPEncryption where- envStringToValue "Unencrypted" = Right Unencrypted- envStringToValue "TLS" = Right TLS- envStringToValue "STARTTLS" = Right STARTTLS- envStringToValue otherwise = Left [i|Expected 'Unencrypted', 'TLS' or 'STARTTLS', got #{otherwise}|]
@@ -1,5 +1,5 @@ module IHP.Environment where-import IHP.Prelude+import Prelude -- | The 'Environment' type is used to switch between development and production configurations. --
@@ -9,32 +9,41 @@ , handleRouterException ) where -import IHP.Prelude hiding (displayException)+import Prelude+import Control.Exception.Safe (SomeException, fromException)+import Control.Monad (when)+import Data.Maybe (mapMaybe, fromMaybe, listToMaybe)+import Data.String.Conversions (cs)+import Data.ByteString (ByteString)+import qualified Data.Text as Text+import IHP.HaskellSupport (isEmpty, forEach, (|>)) import qualified IHP.Controller.Param as Param import qualified IHP.Router.Types as Router import qualified Network.HTTP.Types.Method as Router import qualified Control.Exception as Exception-import qualified Data.Text as Text-import IHP.Controller.RequestContext+import Data.Text (Text)+import Wai.Request.Params.Middleware (Respond) import Network.HTTP.Types (status500, status400) import Network.Wai import Network.HTTP.Types.Header import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html.Renderer.Utf8 as Blaze-import qualified Database.PostgreSQL.Simple as PG-import qualified Data.ByteString.Char8 as ByteString+import qualified Hasql.Errors as HasqlErrors+import qualified Hasql.Pool as HasqlPool import IHP.HSX.QQ (hsx) import qualified IHP.ModelSupport as ModelSupport import IHP.FrameworkConfig import qualified IHP.Environment as Environment import IHP.Controller.Context-import IHP.ApplicationContext import IHP.Controller.NotFound (handleNotFound) import qualified IHP.Log as Log -handleNoResponseReturned :: (Show controller, ?context :: ControllerContext) => controller -> IO ResponseReceived+tshow :: Show a => a -> Text+tshow = Text.pack . show++handleNoResponseReturned :: (Show controller, ?context :: ControllerContext, ?respond :: Respond) => controller -> IO ResponseReceived handleNoResponseReturned controller = do let codeSample :: Text = "render MyView { .. }" let errorMessage = [hsx|@@ -46,10 +55,9 @@ |] let title = [hsx|No response returned in {tshow controller}|]- let RequestContext { respond } = ?context.requestContext- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError ?context.frameworkConfig.environment title errorMessage)) -displayException :: (Show action, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?requestContext :: RequestContext) => SomeException -> action -> Text -> IO ResponseReceived+displayException :: (Show action, ?context :: ControllerContext, ?request :: Request, ?respond :: Respond) => SomeException -> action -> Text -> IO ResponseReceived displayException exception action additionalInfo = do -- Dev handlers display helpful tips on how to resolve the problem let devHandlers =@@ -78,21 +86,18 @@ -- the error message to the stderr output -- when (?context.frameworkConfig.environment == Environment.Production) do- let exceptionTracker = ?applicationContext.frameworkConfig.exceptionTracker.onException- let request = ?requestContext.request--- exceptionTracker (Just request) exception+ let exceptionTracker = ?context.frameworkConfig.exceptionTracker.onException+ exceptionTracker (Just ?request) exception supportingHandlers- |> head+ |> listToMaybe |> fromMaybe displayGenericError -- | Responds to all exceptions with a generic error message. -- -- In dev mode the action and exception is added to the output. -- In production mode nothing is specific is communicated about the exception-genericHandler :: (Show controller, ?context :: ControllerContext) => Exception.SomeException -> controller -> Text -> IO ResponseReceived+genericHandler :: (Show controller, ?context :: ControllerContext, ?respond :: Respond) => Exception.SomeException -> controller -> Text -> IO ResponseReceived genericHandler exception controller additionalInfo = do let errorMessageText = "An exception was raised while running the action " <> tshow controller <> additionalInfo let errorMessageTitle = Exception.displayException exception@@ -108,15 +113,14 @@ let (errorMessage, errorTitle) = if ?context.frameworkConfig.environment == Environment.Development then (devErrorMessage, devTitle) else (prodErrorMessage, prodTitle)- let RequestContext { respond } = ?context.requestContext - respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError errorTitle errorMessage))+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError ?context.frameworkConfig.environment errorTitle errorMessage)) -postgresHandler :: (Show controller, ?context :: ControllerContext) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived)+postgresHandler :: (Show controller, ?context :: ControllerContext, ?respond :: Respond) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived) postgresHandler exception controller additionalInfo = do let- handlePostgresOutdatedError :: Show exception => exception -> H.Html -> IO ResponseReceived- handlePostgresOutdatedError exception errorText = do+ handlePostgresOutdatedError :: Text -> H.Html -> IO ResponseReceived+ handlePostgresOutdatedError errorDetail errorText = do let ihpIdeBaseUrl = ?context.frameworkConfig.ideBaseUrl let title = [hsx|Database looks outdated. {errorText}|] let errorMessage = [hsx|@@ -131,52 +135,79 @@ <h2>Details</h2> <p style="font-size: 16px">The exception was raised while running the action: {tshow controller}{additionalInfo}</p>- <p style="font-family: monospace; font-size: 16px">{tshow exception}</p>+ <p style="font-family: monospace; font-size: 16px">{errorDetail}</p> |]- let RequestContext { respond } = ?context.requestContext- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage)) - handleSqlError :: ModelSupport.EnhancedSqlError -> IO ResponseReceived- handleSqlError exception = do- let ihpIdeBaseUrl = ?context.frameworkConfig.ideBaseUrl- let sqlError = exception.sqlError- let title = [hsx|{sqlError.sqlErrorMsg}|]+ handleServerError :: Text -> [Text] -> HasqlErrors.ServerError -> IO ResponseReceived+ handleServerError sql params (HasqlErrors.ServerError code msg detail hint _position) = do+ let title = [hsx|{msg}|]+ let detailSection = case detail of+ Just d -> [hsx|<p style="font-size: 16px"><strong>Detail:</strong> {d}</p>|]+ Nothing -> mempty+ let hintSection = case hint of+ Just h -> [hsx|<p style="font-size: 16px"><strong>Hint:</strong> {h}</p>|]+ Nothing -> mempty+ let paramsText = Text.intercalate ", " params let errorMessage = [hsx|- <h2>While running the following Query:</h2>+ <h2>While running the following query:</h2> <div style="margin-bottom: 2rem; font-weight: 400;">- <code>{exception.sqlErrorQuery}</code>+ <pre class="ihp-error-code">{sql}</pre> </div> - <h2>With Query Parameters:</h2>+ <h2>With parameters:</h2> <div style="margin-bottom: 2rem; font-weight: 400;">- <code>{exception.sqlErrorQueryParams}</code>+ <code>{paramsText}</code> </div> + {detailSection}+ {hintSection}++ <p style="font-size: 14px; font-family: monospace;">PostgreSQL error code: {code}</p>+ <h2>Details:</h2> <p style="font-size: 16px">The exception was raised while running the action: {tshow controller}{additionalInfo}</p>- <p style="font-family: monospace; font-size: 16px">{tshow exception}</p> |]- let RequestContext { respond } = ?context.requestContext- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))- case fromException exception of- Just (exception :: PG.ResultError) -> Just (handlePostgresOutdatedError exception "The database result does not match the expected type.")- Nothing -> case fromException exception of- -- Catching `relation "..." does not exist`- Just exception@ModelSupport.EnhancedSqlError { sqlError }- | "relation" `ByteString.isPrefixOf` (sqlError.sqlErrorMsg)- && "does not exist" `ByteString.isSuffixOf` (sqlError.sqlErrorMsg)- -> Just (handlePostgresOutdatedError exception "A table is missing.")+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage)) - -- Catching `columns "..." does not exist`- Just exception@ModelSupport.EnhancedSqlError { sqlError }- | "column" `ByteString.isPrefixOf` (sqlError.sqlErrorMsg)- && "does not exist" `ByteString.isSuffixOf` (sqlError.sqlErrorMsg)- -> Just (handlePostgresOutdatedError exception "A column is missing.")- -- Catching other SQL Errors- Just exception -> Just (handleSqlError exception)- Nothing -> Nothing+ handleSessionError :: HasqlErrors.SessionError -> IO ResponseReceived+ handleSessionError sessionError = do+ let title = [hsx|PostgreSQL Error|]+ let errorMessage = [hsx|+ <h2>Details:</h2>+ <p style="font-size: 16px">The exception was raised while running the action: {tshow controller}{additionalInfo}</p>+ <pre class="ihp-error-code">{tshow sessionError}</pre>+ |]+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage)) -patternMatchFailureHandler :: (Show controller, ?context :: ControllerContext) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived)+ case fromException exception of+ Just (ModelSupport.HasqlError (HasqlPool.SessionUsageError sessionError)) -> Just case sessionError of+ -- Statement with a ServerError+ HasqlErrors.StatementSessionError _pipelineSize _stmtIdx sql params _prepared (HasqlErrors.ServerStatementError serverError@(HasqlErrors.ServerError code _msg _ _ _))+ -- 42P01 = undefined_table ("relation ... does not exist")+ | code == "42P01" -> handlePostgresOutdatedError (tshow serverError) "A table is missing."+ -- 42703 = undefined_column ("column ... does not exist")+ | code == "42703" -> handlePostgresOutdatedError (tshow serverError) "A column is missing."+ -- All other server errors on statements+ | otherwise -> handleServerError sql params serverError+ -- Script (multi-statement) with a ServerError+ HasqlErrors.ScriptSessionError sql serverError@(HasqlErrors.ServerError code _msg _ _ _)+ | code == "42P01" -> handlePostgresOutdatedError (tshow serverError) "A table is missing."+ | code == "42703" -> handlePostgresOutdatedError (tshow serverError) "A column is missing."+ | otherwise -> handleServerError sql [] serverError+ -- Any other session error (connection errors, type mismatches, etc.)+ other -> handleSessionError other+ Just (ModelSupport.HasqlError _otherUsageError) -> Just do+ let title = [hsx|Database Connection Error|]+ let errorMessage = [hsx|+ <h2>Details:</h2>+ <p style="font-size: 16px">The exception was raised while running the action: {tshow controller}{additionalInfo}</p>+ <pre class="ihp-error-code">{tshow _otherUsageError}</pre>+ |]+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage))+ Nothing -> Nothing++patternMatchFailureHandler :: (Show controller, ?context :: ControllerContext, ?respond :: Respond) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived) patternMatchFailureHandler exception controller additionalInfo = do case fromException exception of Just (exception :: Exception.PatternMatchFail) -> Just do@@ -194,12 +225,13 @@ codeSample = " action (" <> tshow controller <> ") = do\n renderPlain \"Hello World\"" let title = [hsx|Pattern match failed while executing {tshow controller}|]- let RequestContext { respond } = ?context.requestContext- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage)) Nothing -> Nothing -- Handler for 'IHP.Controller.Param.ParamNotFoundException'-paramNotFoundExceptionHandler :: (Show controller, ?context :: ControllerContext) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived)+-- Only used in dev mode of the app.++paramNotFoundExceptionHandler :: (Show controller, ?context :: ControllerContext, ?request :: Request, ?respond :: Respond) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived) paramNotFoundExceptionHandler exception controller additionalInfo = do case fromException exception of Just (exception@(Param.ParamNotFoundException paramName)) -> Just do@@ -238,8 +270,7 @@ let title = [hsx|Parameter <q>{paramName}</q> not found in the request|]- let RequestContext { respond } = ?context.requestContext- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage)) Just (exception@(Param.ParamCouldNotBeParsedException { name, parserError })) -> Just do let (controllerPath, _) = Text.breakOn ":" (tshow exception) @@ -259,26 +290,21 @@ let title = [hsx|Parameter <q>{name}</q> was invalid|]- let RequestContext { respond } = ?context.requestContext- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage)) Nothing -> Nothing -- Handler for 'IHP.ModelSupport.RecordNotFoundException' -- -- Used only in development mode of the app.-recordNotFoundExceptionHandlerDev :: (Show controller, ?context :: ControllerContext) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived)+recordNotFoundExceptionHandlerDev :: (Show controller, ?context :: ControllerContext, ?respond :: Respond) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived) recordNotFoundExceptionHandlerDev exception controller additionalInfo = case fromException exception of- Just (exception@(ModelSupport.RecordNotFoundException { queryAndParams = (query, params) })) -> Just do+ Just (exception@(ModelSupport.RecordNotFoundException { queryAndParams })) -> Just do let (controllerPath, _) = Text.breakOn ":" (tshow exception) let errorMessage = [hsx| <p> The following SQL was executed:- <pre class="ihp-error-code">{query}</pre>- </p>- <p>- These query parameters have been used:- <pre class="ihp-error-code">{params}</pre>+ <pre class="ihp-error-code">{queryAndParams}</pre> </p> <p>@@ -304,29 +330,24 @@ let title = [hsx|Call to fetchOne failed. No records returned.|]- let RequestContext { respond } = ?context.requestContext- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ ?respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError Environment.Development title errorMessage)) Nothing -> Nothing -- Handler for 'IHP.ModelSupport.RecordNotFoundException' -- -- Used only in production mode of the app. The exception is handled by calling 'handleNotFound'-recordNotFoundExceptionHandlerProd :: (?context :: ControllerContext) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived)+recordNotFoundExceptionHandlerProd :: (?context :: ControllerContext, ?request :: Request, ?respond :: Respond) => SomeException -> controller -> Text -> Maybe (IO ResponseReceived) recordNotFoundExceptionHandlerProd exception controller additionalInfo = case fromException exception of Just (exception@(ModelSupport.RecordNotFoundException {})) ->- let requestContext = ?context.requestContext- in- let ?context = requestContext- in Just (handleNotFound ?context.request ?context.respond)+ Just (handleNotFound ?request ?respond) Nothing -> Nothing -handleRouterException :: (?applicationContext :: ApplicationContext) => SomeException -> Application-handleRouterException exception request respond =- let ?context = ?applicationContext- in case fromException exception of+handleRouterException :: Environment.Environment -> SomeException -> Application+handleRouterException environment exception request respond =+ case fromException exception of Just Router.NoConstructorMatched { expectedType, value, field } -> do- let routingError = if ?context.frameworkConfig.environment == Environment.Development+ let routingError = if environment == Environment.Development then [hsx|<p>Routing failed with: {tshow exception}</p>|] else "" @@ -340,22 +361,17 @@ let title = case value of Just value -> [hsx|Expected <strong>{expectedType}</strong> for field <strong>{field}</strong> but got <q>{value}</q>|] Nothing -> [hsx|The action was called without the required <q>{field}</q> parameter|]- respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError environment title errorMessage)) Just Router.BadType { expectedType, value = Just value, field } -> do let errorMessage = [hsx| <p>Routing failed with: {tshow exception}</p> |] let title = [hsx|Query parameter <q>{field}</q> needs to be a <q>{expectedType}</q> but got <q>{value}</q>|]- respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError environment title errorMessage)) _ -> case fromException exception of Just Router.UnexpectedMethodException { allowedMethods = [Router.DELETE], method = Router.GET } -> do let exampleLink :: Text = "<a href={DeleteProjectAction} class=\"js-delete\">Delete Project</a>"- let formExample :: Text = cs [plain|-<form method="POST" action={DeleteProjectAction}>- <input type="hidden" name="_method" value="DELETE"/>- <button type="submit">Delete Project</button>-</form>- |]+ let formExample :: Text = "<form method=\"POST\" action={DeleteProjectAction}>\n <input type=\"hidden\" name=\"_method\" value=\"DELETE\"/>\n <button type=\"submit\">Delete Project</button>\n</form>" let errorMessage = [hsx| <p> You cannot directly link to Delete Action.@@ -380,7 +396,7 @@ </p> |] let title = [hsx|Action was called from a GET request, but needs to be called as a DELETE request|]- respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError environment title errorMessage)) Just Router.UnexpectedMethodException { allowedMethods = [Router.POST], method = Router.GET } -> do let errorMessage = [hsx| <p>@@ -394,7 +410,7 @@ </p> |] let title = [hsx|Action was called from a GET request, but needs to be called as a POST request|]- respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError environment title errorMessage)) Just Router.UnexpectedMethodException { allowedMethods, method } -> do let errorMessage = [hsx| <p>Routing failed with: {tshow exception}</p>@@ -404,7 +420,7 @@ </p> |] let title = [hsx|Action was called with a {method} request, but needs to be called with one of these request methods: <q>{allowedMethods}</q>|]- respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ respond $ responseBuilder status400 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError environment title errorMessage)) _ -> do let errorMessage = [hsx| Routing failed with: {tshow exception}@@ -413,11 +429,11 @@ <p>Are you trying to do a DELETE action, but your link is missing class="js-delete"?</p> |] let title = H.text "Routing failed"- respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError title errorMessage))+ respond $ responseBuilder status500 [(hContentType, "text/html")] (Blaze.renderHtmlBuilder (renderError environment title errorMessage)) -renderError :: forall context. (?context :: context, ConfigProvider context) => H.Html -> H.Html -> H.Html-renderError errorTitle view = [hsx|+renderError :: Environment.Environment -> H.Html -> H.Html -> H.Html+renderError environment errorTitle view = [hsx| <!DOCTYPE html> <html lang="en"> <head>@@ -490,7 +506,7 @@ </html> |] where- shouldShowHelpFooter = ?context.frameworkConfig.environment == Environment.Development+ shouldShowHelpFooter = environment == Environment.Development helpFooter = [hsx| <div class="ihp-error-other-solutions"> <a href="https://stackoverflow.com/questions/tagged/ihp" target="_blank">Ask the IHP Community on StackOverflow</a>
@@ -12,7 +12,6 @@ ( findManyBy , findMaybeBy , findBy-, In (In) , genericFetchId , genericfetchIdOneOrNothing , genericFetchIdOne@@ -22,111 +21,90 @@ , genericFetchIdsOne , fetchCount , fetchExists-, fetchSQLQuery , fetchLatest , fetchLatestBy ) where import IHP.Prelude-import Database.PostgreSQL.Simple.Types (Query (Query))-import Database.PostgreSQL.Simple.FromField hiding (Field, name)-import Database.PostgreSQL.Simple.ToField-import qualified Database.PostgreSQL.Simple as PG import IHP.ModelSupport import IHP.QueryBuilder+import IHP.Hasql.FromRow (FromRowHasql(..), HasqlDecodeColumn(..))+import IHP.QueryBuilder.HasqlCompiler (buildStatement)+import qualified Hasql.Decoders as Decoders+import Hasql.Implicits.Encoders (DefaultParamEncoder)+import IHP.Fetch.Statement (buildQueryListStatement, buildQueryMaybeStatement, buildCountStatement, buildExistsStatement) class Fetchable fetchable model | fetchable -> model where type FetchResult fetchable model- fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (FetchResult fetchable model)- fetchOneOrNothing :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO (Maybe model)- fetchOne :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => fetchable -> IO model+ fetch :: (Table model, FromRowHasql model, ?modelContext :: ModelContext) => fetchable -> IO (FetchResult fetchable model)+ fetchOneOrNothing :: (Table model, FromRowHasql model, ?modelContext :: ModelContext) => fetchable -> IO (Maybe model)+ fetchOne :: (Table model, FromRowHasql model, ?modelContext :: ModelContext) => fetchable -> IO model -- The instance declaration had to be split up because a type variable ranging over HasQueryBuilder instances is not allowed in the declaration of the associated type. The common*-functions reduce the redundancy to the necessary minimum. instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (QueryBuilder table) model where type instance FetchResult (QueryBuilder table) model = [model]- {-# INLINE fetch #-}- fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => QueryBuilder table -> IO [model]- fetch = commonFetch + fetch :: (Table model, FromRowHasql model, ?modelContext :: ModelContext) => QueryBuilder table -> IO [model]+ fetch = commonFetch - {-# INLINE fetchOneOrNothing #-}- fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => QueryBuilder table -> IO (Maybe model)+ fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => QueryBuilder table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing - {-# INLINE fetchOne #-}- fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => QueryBuilder table -> IO model+ fetchOne :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => QueryBuilder table -> IO model fetchOne = commonFetchOne instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (JoinQueryBuilderWrapper r table) model where type instance FetchResult (JoinQueryBuilderWrapper r table) model = [model]- {-# INLINE fetch #-}- fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => JoinQueryBuilderWrapper r table -> IO [model]- fetch = commonFetch + fetch :: (Table model, FromRowHasql model, ?modelContext :: ModelContext) => JoinQueryBuilderWrapper r table -> IO [model]+ fetch = commonFetch - {-# INLINE fetchOneOrNothing #-}- fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => JoinQueryBuilderWrapper r table -> IO (Maybe model)+ fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => JoinQueryBuilderWrapper r table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing - {-# INLINE fetchOne #-}- fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => JoinQueryBuilderWrapper r table -> IO model+ fetchOne :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => JoinQueryBuilderWrapper r table -> IO model fetchOne = commonFetchOne instance (model ~ GetModelByTableName table, KnownSymbol table) => Fetchable (NoJoinQueryBuilderWrapper table) model where type instance FetchResult (NoJoinQueryBuilderWrapper table) model = [model]- {-# INLINE fetch #-}- fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => NoJoinQueryBuilderWrapper table -> IO [model]- fetch = commonFetch + fetch :: (Table model, FromRowHasql model, ?modelContext :: ModelContext) => NoJoinQueryBuilderWrapper table -> IO [model]+ fetch = commonFetch - {-# INLINE fetchOneOrNothing #-}- fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => NoJoinQueryBuilderWrapper table -> IO (Maybe model)+ fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => NoJoinQueryBuilderWrapper table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing - {-# INLINE fetchOne #-}- fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => NoJoinQueryBuilderWrapper table -> IO model+ fetchOne :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => NoJoinQueryBuilderWrapper table -> IO model fetchOne = commonFetchOne -instance (model ~ GetModelByTableName table, KnownSymbol table, FromField value, KnownSymbol foreignTable, foreignModel ~ GetModelByTableName foreignTable, KnownSymbol columnName, HasField columnName foreignModel value, HasQueryBuilder (LabeledQueryBuilderWrapper foreignTable columnName value) NoJoins) => Fetchable (LabeledQueryBuilderWrapper foreignTable columnName value table) model where+instance (model ~ GetModelByTableName table, KnownSymbol table, HasqlDecodeColumn value, KnownSymbol foreignTable, foreignModel ~ GetModelByTableName foreignTable, KnownSymbol columnName, HasField columnName foreignModel value, HasQueryBuilder (LabeledQueryBuilderWrapper foreignTable columnName value) NoJoins) => Fetchable (LabeledQueryBuilderWrapper foreignTable columnName value table) model where type instance FetchResult (LabeledQueryBuilderWrapper foreignTable columnName value table) model = [LabeledData value model]- -- fetch needs to return a list of labeled data. The - {-# INLINE fetch #-}- fetch :: (Table model, PG.FromRow model, ?modelContext :: ModelContext) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO [LabeledData value model]+ fetch :: (Table model, FromRowHasql model, ?modelContext :: ModelContext) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO [LabeledData value model] fetch !queryBuilderProvider = do- let !(theQuery, theParameters) = queryBuilderProvider- |> toSQL- trackTableRead (tableNameByteString @model)- sqlQuery @_ @(LabeledData value model) (Query $ cs theQuery) theParameters+ trackTableRead (tableName @model)+ let pool = ?modelContext.hasqlPool+ let statement = buildStatement (buildQuery queryBuilderProvider) (Decoders.rowList (hasqlRowDecoder @(LabeledData value model)))+ sqlStatementHasql pool () statement - {-# INLINE fetchOneOrNothing #-}- fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO (Maybe model)+ fetchOneOrNothing :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO (Maybe model) fetchOneOrNothing = commonFetchOneOrNothing - {-# INLINE fetchOne #-}- fetchOne :: (?modelContext :: ModelContext) => (Table model, PG.FromRow model) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO model+ fetchOne :: (?modelContext :: ModelContext) => (Table model, FromRowHasql model) => LabeledQueryBuilderWrapper foreignTable columnName value table -> IO model fetchOne = commonFetchOne -{-# INLINE commonFetch #-}-commonFetch :: forall model table queryBuilderProvider joinRegister. (Table model, HasQueryBuilder queryBuilderProvider joinRegister, model ~ GetModelByTableName table, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext) => queryBuilderProvider table -> IO [model]+commonFetch :: forall model table queryBuilderProvider joinRegister. (Table model, HasQueryBuilder queryBuilderProvider joinRegister, model ~ GetModelByTableName table, KnownSymbol table, FromRowHasql model, ?modelContext :: ModelContext) => queryBuilderProvider table -> IO [model] commonFetch !queryBuilder = do- let !(theQuery, theParameters) = queryBuilder- |> toSQL- trackTableRead (tableNameByteString @model)- sqlQuery (Query $ cs theQuery) theParameters+ trackTableRead (tableName @model)+ let pool = ?modelContext.hasqlPool+ sqlStatementHasql pool () (buildQueryListStatement queryBuilder) -{-# INLINE commonFetchOneOrNothing #-}-commonFetchOneOrNothing :: forall model table queryBuilderProvider joinRegister. (?modelContext :: ModelContext) => (Table model, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model) => queryBuilderProvider table -> IO (Maybe model)+commonFetchOneOrNothing :: forall model table queryBuilderProvider joinRegister. (?modelContext :: ModelContext) => (Table model, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister, FromRowHasql model, model ~ GetModelByTableName table) => queryBuilderProvider table -> IO (Maybe model) commonFetchOneOrNothing !queryBuilder = do- let !(theQuery, theParameters) = queryBuilder- |> buildQuery- |> setJust #limitClause "LIMIT 1"- |> toSQL'- trackTableRead (tableNameByteString @model)- results <- sqlQuery (Query $ cs theQuery) theParameters- pure $ listToMaybe results+ trackTableRead (tableName @model)+ let pool = ?modelContext.hasqlPool+ sqlStatementHasql pool () (buildQueryMaybeStatement queryBuilder) -{-# INLINE commonFetchOne #-}-commonFetchOne :: forall model table queryBuilderProvider joinRegister. (?modelContext :: ModelContext) => (Table model, KnownSymbol table, Fetchable (queryBuilderProvider table) model, HasQueryBuilder queryBuilderProvider joinRegister, PG.FromRow model) => queryBuilderProvider table -> IO model+commonFetchOne :: forall model table queryBuilderProvider joinRegister. (?modelContext :: ModelContext) => (Table model, KnownSymbol table, Fetchable (queryBuilderProvider table) model, HasQueryBuilder queryBuilderProvider joinRegister, FromRowHasql model) => queryBuilderProvider table -> IO model commonFetchOne !queryBuilder = do maybeModel <- fetchOneOrNothing queryBuilder case maybeModel of@@ -149,12 +127,9 @@ -- > -- SELECT COUNT(*) FROM projects WHERE is_active = true fetchCount :: forall table queryBuilderProvider joinRegister. (?modelContext :: ModelContext, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> IO Int fetchCount !queryBuilder = do- let !(theQuery', theParameters) = toSQL' (buildQuery queryBuilder)- let theQuery = "SELECT COUNT(*) FROM (" <> theQuery' <> ") AS _count_values"- trackTableRead (symbolToByteString @table)- [PG.Only count] <- sqlQuery (Query $! cs theQuery) theParameters- pure count-{-# INLINE fetchCount #-}+ trackTableRead (symbolToText @table)+ let pool = ?modelContext.hasqlPool+ fromIntegral <$> sqlStatementHasql pool () (buildCountStatement queryBuilder) -- | Checks whether the query has any results. --@@ -168,84 +143,57 @@ -- > -- SELECT EXISTS (SELECT * FROM messages WHERE is_unread = true) fetchExists :: forall table queryBuilderProvider joinRegister. (?modelContext :: ModelContext, KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> IO Bool fetchExists !queryBuilder = do- let !(theQuery', theParameters) = toSQL' (buildQuery queryBuilder)- let theQuery = "SELECT EXISTS (" <> theQuery' <> ") AS _exists_values"- trackTableRead (symbolToByteString @table)- [PG.Only exists] <- sqlQuery (Query $! cs theQuery) theParameters- pure exists-{-# INLINE fetchExists #-}+ trackTableRead (symbolToText @table)+ let pool = ?modelContext.hasqlPool+ sqlStatementHasql pool () (buildExistsStatement queryBuilder) -{-# INLINE genericFetchId #-}-genericFetchId :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO [model]+genericFetchId :: forall table model. (Table model, KnownSymbol table, FromRowHasql model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table, FilterPrimaryKey table) => Id' table -> IO [model] genericFetchId !id = query @model |> filterWhereId id |> fetch -{-# INLINE genericfetchIdOneOrNothing #-}-genericfetchIdOneOrNothing :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO (Maybe model)+genericfetchIdOneOrNothing :: forall table model. (Table model, KnownSymbol table, FromRowHasql model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table, FilterPrimaryKey table) => Id' table -> IO (Maybe model) genericfetchIdOneOrNothing !id = query @model |> filterWhereId id |> fetchOneOrNothing -{-# INLINE genericFetchIdOne #-}-genericFetchIdOne :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, FilterPrimaryKey table, model ~ GetModelByTableName table, GetTableName model ~ table) => Id' table -> IO model+genericFetchIdOne :: forall table model. (Table model, KnownSymbol table, FromRowHasql model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table, FilterPrimaryKey table) => Id' table -> IO model genericFetchIdOne !id = query @model |> filterWhereId id |> fetchOne -{-# INLINE genericFetchIds #-}-genericFetchIds :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table) => [Id model] -> IO [model]+genericFetchIds :: forall table model. (Table model, KnownSymbol table, FromRowHasql model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table, DefaultParamEncoder [PrimaryKey (GetTableName model)]) => [Id model] -> IO [model] genericFetchIds !ids = query @model |> filterWhereIdIn ids |> fetch -{-# INLINE genericfetchIdsOneOrNothing #-}-genericfetchIdsOneOrNothing :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table) => [Id model] -> IO (Maybe model)+genericfetchIdsOneOrNothing :: forall table model. (Table model, KnownSymbol table, FromRowHasql model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table, DefaultParamEncoder [PrimaryKey (GetTableName model)]) => [Id model] -> IO (Maybe model) genericfetchIdsOneOrNothing !ids = query @model |> filterWhereIdIn ids |> fetchOneOrNothing -{-# INLINE genericFetchIdsOne #-}-genericFetchIdsOne :: forall table model. (Table model, KnownSymbol table, PG.FromRow model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table) => [Id model] -> IO model+genericFetchIdsOne :: forall table model. (Table model, KnownSymbol table, FromRowHasql model, ?modelContext :: ModelContext, model ~ GetModelByTableName table, GetTableName model ~ table, DefaultParamEncoder [PrimaryKey (GetTableName model)]) => [Id model] -> IO model genericFetchIdsOne !ids = query @model |> filterWhereIdIn ids |> fetchOne -{-# INLINE findBy #-} findBy !field !value !queryBuilder = queryBuilder |> filterWhere (field, value) |> fetchOne -{-# INLINE findMaybeBy #-} findMaybeBy !field !value !queryBuilder = queryBuilder |> filterWhere (field, value) |> fetchOneOrNothing --findManyBy :: (?modelContext :: ModelContext, PG.FromRow model, KnownSymbol name, ToField value, HasField name value model) => Proxy name -> value -> QueryBuilder model -> IO [model]-{-# INLINE findManyBy #-} findManyBy !field !value !queryBuilder = queryBuilder |> filterWhere (field, value) |> fetch -- Step.findOneByWorkflowId id == queryBuilder |> findBy #templateId id instance (model ~ GetModelById (Id' table), GetTableName model ~ table, FilterPrimaryKey table) => Fetchable (Id' table) model where type FetchResult (Id' table) model = model- {-# INLINE fetch #-} fetch = genericFetchIdOne- {-# INLINE fetchOneOrNothing #-} fetchOneOrNothing = genericfetchIdOneOrNothing- {-# INLINE fetchOne #-} fetchOne = genericFetchIdOne instance (model ~ GetModelById (Id' table), GetTableName model ~ table, FilterPrimaryKey table) => Fetchable (Maybe (Id' table)) model where type FetchResult (Maybe (Id' table)) model = [model]- {-# INLINE fetch #-} fetch (Just a) = genericFetchId a fetch Nothing = pure []- {-# INLINE fetchOneOrNothing #-} fetchOneOrNothing Nothing = pure Nothing fetchOneOrNothing (Just a) = genericfetchIdOneOrNothing a- {-# INLINE fetchOne #-} fetchOne (Just a) = genericFetchIdOne a fetchOne Nothing = error "Fetchable (Maybe Id): Failed to fetch because given id is 'Nothing', 'Just id' was expected" -instance (model ~ GetModelById (Id' table), GetModelByTableName table ~ model, GetTableName model ~ table) => Fetchable [Id' table] model where+instance (model ~ GetModelById (Id' table), GetModelByTableName table ~ model, GetTableName model ~ table, DefaultParamEncoder [PrimaryKey table]) => Fetchable [Id' table] model where type FetchResult [Id' table] model = [model]- {-# INLINE fetch #-} fetch = genericFetchIds- {-# INLINE fetchOneOrNothing #-} fetchOneOrNothing = genericfetchIdsOneOrNothing- {-# INLINE fetchOne #-} fetchOne = genericFetchIdsOne -fetchSQLQuery :: (PG.FromRow model, ?modelContext :: ModelContext) => SQLQuery -> IO [model]-fetchSQLQuery theQuery = do- let (sql, theParameters) = toSQL' theQuery- trackTableRead (theQuery.selectFrom)- sqlQuery (Query $ cs sql) theParameters- -- | Returns the latest record or Nothing -- -- __Example:__@@ -270,7 +218,7 @@ , HasField "createdAt" model UTCTime , Fetchable (queryBuilderProvider table) model , Table model- , FromRow model+ , FromRowHasql model ) => queryBuilderProvider table -> IO (Maybe model) fetchLatest queryBuilder = queryBuilder |> fetchLatestBy #createdAt @@ -301,7 +249,7 @@ , HasField createdAt model UTCTime , Fetchable (queryBuilderProvider table) model , Table model- , FromRow model+ , FromRowHasql model ) => Proxy createdAt -> queryBuilderProvider table -> IO (Maybe model) fetchLatestBy field queryBuilder = queryBuilder
@@ -0,0 +1,56 @@+{-# LANGUAGE AllowAmbiguousTypes, ScopedTypeVariables, TypeApplications, DataKinds, TypeFamilies, PolyKinds, ConstraintKinds, TypeOperators, BangPatterns, FlexibleContexts #-}+{-|+Module: IHP.Fetch.Statement+Description: Prepared statements for fetch operations+Copyright: (c) digitally induced GmbH, 2025++Internal module containing hasql 'Statement' definitions shared by+'IHP.Fetch' and 'IHP.FetchPipelined'.+-}+module IHP.Fetch.Statement+( buildQueryListStatement+, buildQueryMaybeStatement+, buildCountStatement+, buildExistsStatement+) where++import Prelude+import IHP.ModelSupport (Table(..), GetModelByTableName)+import IHP.Hasql.FromRow (FromRowHasql(..))+import qualified Hasql.Statement as Hasql+import qualified Hasql.Decoders as Decoders+import IHP.QueryBuilder.Types (HasQueryBuilder(..), SQLQuery(..))+import IHP.QueryBuilder.Compiler (buildQuery)+import IHP.QueryBuilder.HasqlCompiler (buildStatement, buildWrappedStatement)+import GHC.TypeLits (KnownSymbol)+import Data.Int (Int64)++-- | Build a statement that fetches all rows matching a query builder.+buildQueryListStatement :: forall model table queryBuilderProvider joinRegister.+ ( Table model, HasQueryBuilder queryBuilderProvider joinRegister+ , model ~ GetModelByTableName table, KnownSymbol table, FromRowHasql model+ ) => queryBuilderProvider table -> Hasql.Statement () [model]+buildQueryListStatement !queryBuilder =+ buildStatement (buildQuery queryBuilder) (Decoders.rowList (hasqlRowDecoder @model))++-- | Build a statement that fetches at most one row (adds LIMIT 1).+buildQueryMaybeStatement :: forall model table queryBuilderProvider joinRegister.+ ( Table model, HasQueryBuilder queryBuilderProvider joinRegister+ , model ~ GetModelByTableName table, KnownSymbol table, FromRowHasql model+ ) => queryBuilderProvider table -> Hasql.Statement () (Maybe model)+buildQueryMaybeStatement !queryBuilder =+ buildStatement ((buildQuery queryBuilder) { limitClause = Just 1 }) (Decoders.rowMaybe (hasqlRowDecoder @model))++-- | Build a @SELECT COUNT(*)@ statement wrapping a query builder.+buildCountStatement :: forall table queryBuilderProvider joinRegister.+ ( KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister+ ) => queryBuilderProvider table -> Hasql.Statement () Int64+buildCountStatement !queryBuilder =+ buildWrappedStatement "SELECT COUNT(*) FROM (" (buildQuery queryBuilder) ") AS _count_values" (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int8)))++-- | Build a @SELECT EXISTS@ statement wrapping a query builder.+buildExistsStatement :: forall table queryBuilderProvider joinRegister.+ ( KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister+ ) => queryBuilderProvider table -> Hasql.Statement () Bool+buildExistsStatement !queryBuilder =+ buildWrappedStatement "SELECT EXISTS (" (buildQuery queryBuilder) ") AS _exists_values" (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
@@ -0,0 +1,174 @@+{-# LANGUAGE ApplicativeDo, BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, FlexibleContexts, AllowAmbiguousTypes #-}++{-|+Module: IHP.FetchPipelined+Description: Fetch multiple independent queries in a single database round trip+Copyright: (c) digitally induced GmbH, 2026++Uses PostgreSQL's pipeline mode (via hasql) to send multiple independent queries+in a single network round trip. This is especially beneficial for cloud database+deployments where round-trip latency is 1-5ms.++Compose queries using @do@ notation (via @ApplicativeDo@):++> (users, posts) <- pipeline do+> users <- query @User |> fetchPipelined+> posts <- query @Post |> orderByDesc #createdAt |> fetchPipelined+> pure (users, posts)++'Pipeline' is 'Applicative' but NOT 'Monad', which enforces at the type level+that only independent queries can be pipelined. @ApplicativeDo@ desugars the+@do@ block into applicative operations, so each line runs as a separate query+in the same pipeline batch.+-}+module IHP.FetchPipelined+( fetchPipelined+, fetchOneOrNothingPipelined+, fetchCountPipelined+, fetchExistsPipelined+, pipeline+, Pipeline.Pipeline+) where++import IHP.Prelude+import IHP.ModelSupport+import IHP.QueryBuilder+import IHP.Hasql.FromRow (FromRowHasql(..))+import IHP.Fetch.Statement (buildQueryListStatement, buildQueryMaybeStatement, buildCountStatement, buildExistsStatement)+import qualified Hasql.Decoders as Decoders+import qualified Hasql.Encoders as Encoders+import qualified Hasql.Pipeline as Pipeline+import qualified Hasql.Session as HasqlSession+import qualified Hasql.Statement as HasqlStatement+import qualified IHP.Log as Log+import IHP.Hasql.Pool (usePoolWithRetry)+import Data.Functor.Contravariant (contramap)+import Data.Functor.Contravariant.Divisible (conquer)++-- | Convert a query builder into a 'Pipeline' step returning all matching rows.+--+-- __Example:__ Fetching users and posts in a single round trip+--+-- > (users, posts) <- pipeline do+-- > users <- query @User |> filterWhere (#active, True) |> fetchPipelined+-- > posts <- query @Post |> orderByDesc #createdAt |> fetchPipelined+-- > pure (users, posts)+fetchPipelined :: forall model table queryBuilderProvider joinRegister.+ ( Table model+ , HasQueryBuilder queryBuilderProvider joinRegister+ , model ~ GetModelByTableName table+ , KnownSymbol table+ , FromRowHasql model+ ) => queryBuilderProvider table -> Pipeline.Pipeline [model]+fetchPipelined !queryBuilder = Pipeline.statement () (buildQueryListStatement queryBuilder)+{-# INLINE fetchPipelined #-}++-- | Convert a query builder into a 'Pipeline' step returning at most one row.+--+-- __Example:__+--+-- > (maybeUser, posts) <- pipeline do+-- > maybeUser <- query @User |> filterWhere (#email, email) |> fetchOneOrNothingPipelined+-- > posts <- query @Post |> fetchPipelined+-- > pure (maybeUser, posts)+fetchOneOrNothingPipelined :: forall model table queryBuilderProvider joinRegister.+ ( Table model+ , HasQueryBuilder queryBuilderProvider joinRegister+ , model ~ GetModelByTableName table+ , KnownSymbol table+ , FromRowHasql model+ ) => queryBuilderProvider table -> Pipeline.Pipeline (Maybe model)+fetchOneOrNothingPipelined !queryBuilder = Pipeline.statement () (buildQueryMaybeStatement queryBuilder)+{-# INLINE fetchOneOrNothingPipelined #-}++-- | Convert a query builder into a 'Pipeline' step returning a count.+--+-- __Example:__+--+-- > (users, userCount) <- pipeline do+-- > users <- query @User |> fetchPipelined+-- > userCount <- query @User |> filterWhere (#active, True) |> fetchCountPipelined+-- > pure (users, userCount)+fetchCountPipelined :: forall table queryBuilderProvider joinRegister.+ ( KnownSymbol table+ , HasQueryBuilder queryBuilderProvider joinRegister+ ) => queryBuilderProvider table -> Pipeline.Pipeline Int+fetchCountPipelined !queryBuilder = fromIntegral <$> Pipeline.statement () (buildCountStatement queryBuilder)+{-# INLINE fetchCountPipelined #-}++-- | Convert a query builder into a 'Pipeline' step returning a boolean.+--+-- __Example:__+--+-- > (users, hasUnread) <- pipeline do+-- > users <- query @User |> fetchPipelined+-- > hasUnread <- query @Message |> filterWhere (#isUnread, True) |> fetchExistsPipelined+-- > pure (users, hasUnread)+fetchExistsPipelined :: forall table queryBuilderProvider joinRegister.+ ( KnownSymbol table+ , HasQueryBuilder queryBuilderProvider joinRegister+ ) => queryBuilderProvider table -> Pipeline.Pipeline Bool+fetchExistsPipelined !queryBuilder = Pipeline.statement () (buildExistsStatement queryBuilder)+{-# INLINE fetchExistsPipelined #-}++-- | Execute a 'Pipeline' in a single database round trip.+--+-- When row-level security (RLS) is enabled, the pipeline is automatically wrapped+-- with @set_config@ / reset statements to preserve the request's RLS context.+-- These are included in the same pipeline batch, adding no extra round trips.+--+-- __Example:__+--+-- > action DashboardAction = do+-- > (users, posts, commentCount) <- pipeline do+-- > users <- query @User |> fetchPipelined+-- > posts <- query @Post |> orderByDesc #createdAt |> limit 10 |> fetchPipelined+-- > commentCount <- query @Comment |> fetchCountPipelined+-- > pure (users, posts, commentCount)+-- > render DashboardView { .. }+pipeline :: (?modelContext :: ModelContext) => Pipeline.Pipeline a -> IO a+pipeline thePipeline = do+ let pool = ?modelContext.hasqlPool+ -- When RLS is enabled and we're not already in a transaction, wrap the+ -- pipeline with session-scoped set_config/reset statements. These are+ -- part of the same pipeline batch so they add no extra round trips.+ -- In pipeline mode the server processes statements sequentially, so the+ -- set_config takes effect before the user queries execute.+ let effectivePipeline = case (?modelContext.transactionRunner, ?modelContext.rowLevelSecurity) of+ (Nothing, Just RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId }) ->+ (\_ a _ -> a)+ <$> Pipeline.statement (rlsAuthenticatedRole, rlsUserId) setRLSConfigPipelineStatement+ <*> thePipeline+ <*> Pipeline.statement () resetRLSConfigPipelineStatement+ _ -> thePipeline+ let session = HasqlSession.pipeline effectivePipeline+ let ?context = ?modelContext+ let currentLogLevel = ?modelContext.logger.level+ let runQuery = case ?modelContext.transactionRunner of+ Just (TransactionRunner runner) -> runner session+ Nothing -> usePoolWithRetry pool session+ logQueryTiming currentLogLevel "🔍 Pipeline" runQuery+{-# INLINABLE pipeline #-}++-- | Session-scoped RLS config for pipeline mode.+--+-- Uses @is_local = false@ (session-scoped) instead of @true@ (transaction-local)+-- because pipeline mode runs each statement in its own implicit transaction.+-- The companion 'resetRLSConfigPipelineStatement' resets these at the end of+-- the pipeline batch.+setRLSConfigPipelineStatement :: HasqlStatement.Statement (Text, Text) ()+setRLSConfigPipelineStatement = HasqlStatement.preparable+ "SELECT set_config('role', $1, false), set_config('rls.ihp_user_id', $2, false)"+ (contramap fst (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap snd (Encoders.param (Encoders.nonNullable Encoders.text)))+ (Decoders.singleRow (Decoders.column (Decoders.nullable Decoders.text) *> Decoders.column (Decoders.nullable Decoders.text) *> pure ()))++-- | Reset role and RLS user to connection defaults after the pipeline completes.+--+-- Uses @session_user@ to restore the original connection role, matching+-- the behavior of @RESET ROLE@.+resetRLSConfigPipelineStatement :: HasqlStatement.Statement () ()+resetRLSConfigPipelineStatement = HasqlStatement.preparable+ "SELECT set_config('role', session_user::text, false), set_config('rls.ihp_user_id', '', false)"+ conquer+ (Decoders.singleRow (Decoders.column (Decoders.nullable Decoders.text) *> Decoders.column (Decoders.nullable Decoders.text) *> pure ()))
@@ -11,16 +11,16 @@ module IHP.FetchRelated (fetchRelated, collectionFetchRelated, collectionFetchRelatedOrNothing, fetchRelatedOrNothing, maybeFetchRelatedOrNothing) where import IHP.Prelude-import Database.PostgreSQL.Simple.ToField-import qualified Database.PostgreSQL.Simple as PG import IHP.ModelSupport (Include, Id', PrimaryKey, GetModelByTableName, Table) import IHP.QueryBuilder import IHP.Fetch+import IHP.Hasql.FromRow (FromRowHasql)+import Hasql.Implicits.Encoders (DefaultParamEncoder) -- | This class provides the collectionFetchRelated function -- -- This function is provided by this class as we have to deal with two cases:--- +-- -- 1. the related field is a id, e.g. like the company ids in @users |> collectionFetchRelated #companyId@ -- 2. the related field is a query builder, e.g. in @posts |> collectionFetchRelated #comments@ class CollectionFetchRelated relatedFieldValue relatedModel where@@ -30,14 +30,14 @@ UpdateField relatedField model (Include relatedField model) relatedFieldValue (FetchResult relatedFieldValue relatedModel), Fetchable relatedFieldValue relatedModel, KnownSymbol (GetTableName relatedModel),- PG.FromRow relatedModel,+ FromRowHasql relatedModel, KnownSymbol relatedField ) => Proxy relatedField -> [model] -> IO [Include relatedField model] -- | This class provides the collectionFetchRelatedOrNothing function -- -- This function is provided by this class as we have to deal with two cases:--- +-- -- 1. the related field is an id, e.g. like the company ids in @users |> collectionFetchRelated #companyId@ -- 2. the related field is a query builder, e.g. in @posts |> collectionFetchRelated #comments@ class CollectionFetchRelatedOrNothing relatedFieldValue relatedModel where@@ -47,7 +47,7 @@ UpdateField relatedField model (Include relatedField model) (Maybe relatedFieldValue) (Maybe (FetchResult relatedFieldValue relatedModel)), Fetchable relatedFieldValue relatedModel, KnownSymbol (GetTableName relatedModel),- PG.FromRow relatedModel,+ FromRowHasql relatedModel, KnownSymbol relatedField ) => Proxy relatedField -> [model] -> IO [Include relatedField model] @@ -67,12 +67,12 @@ -- > SELECT * FROM companies WHERE id IN (?) instance ( Eq (PrimaryKey tableName)- , ToField (PrimaryKey tableName) , Show (PrimaryKey tableName) , HasField "id" relatedModel (Id' tableName) , relatedModel ~ GetModelByTableName (GetTableName relatedModel) , GetTableName relatedModel ~ tableName , Table relatedModel+ , DefaultParamEncoder [PrimaryKey tableName] ) => CollectionFetchRelated (Id' tableName) relatedModel where collectionFetchRelated :: forall model relatedField. ( ?modelContext :: ModelContext,@@ -80,7 +80,7 @@ UpdateField relatedField model (Include relatedField model) (Id' tableName) (FetchResult (Id' tableName) relatedModel), Fetchable (Id' tableName) relatedModel, KnownSymbol (GetTableName relatedModel),- PG.FromRow relatedModel,+ FromRowHasql relatedModel, KnownSymbol relatedField, Table relatedModel ) => Proxy relatedField -> [model] -> IO [Include relatedField model]@@ -119,11 +119,11 @@ -- > SELECT * FROM companies WHERE id IN (?) instance ( Eq (PrimaryKey tableName)- , ToField (PrimaryKey tableName) , HasField "id" relatedModel (Id' tableName) , relatedModel ~ GetModelByTableName (GetTableName relatedModel) , GetTableName relatedModel ~ tableName , Table relatedModel+ , DefaultParamEncoder [PrimaryKey tableName] ) => CollectionFetchRelatedOrNothing (Id' tableName) relatedModel where collectionFetchRelatedOrNothing :: forall model relatedField. ( ?modelContext :: ModelContext,@@ -131,7 +131,7 @@ UpdateField relatedField model (Include relatedField model) (Maybe (Id' tableName)) (Maybe (FetchResult (Id' tableName) relatedModel)), Fetchable (Id' tableName) relatedModel, KnownSymbol (GetTableName relatedModel),- PG.FromRow relatedModel,+ FromRowHasql relatedModel, KnownSymbol relatedField ) => Proxy relatedField -> [model] -> IO [Include relatedField model] collectionFetchRelatedOrNothing relatedField model = do@@ -169,7 +169,7 @@ UpdateField relatedField model (Include relatedField model) (QueryBuilder relatedTable) (FetchResult (QueryBuilder relatedTable) relatedModel), Fetchable (QueryBuilder relatedTable) relatedModel, KnownSymbol (GetTableName relatedModel),- PG.FromRow relatedModel,+ FromRowHasql relatedModel, KnownSymbol relatedField ) => Proxy relatedField -> [model] -> IO [Include relatedField model] collectionFetchRelated relatedField models = do@@ -208,7 +208,7 @@ ?modelContext :: ModelContext, UpdateField field model (Include field model) fieldValue (FetchResult fieldValue fetchModel), HasField field model fieldValue,- PG.FromRow fetchModel,+ FromRowHasql fetchModel, KnownSymbol (GetTableName fetchModel), Fetchable fieldValue fetchModel, Table fetchModel@@ -223,7 +223,7 @@ ?modelContext :: ModelContext, UpdateField field model (Include field model) (Maybe fieldValue) (Maybe (FetchResult fieldValue fetchModel)), HasField field model (Maybe fieldValue),- PG.FromRow fetchModel,+ FromRowHasql fetchModel, KnownSymbol (GetTableName fetchModel), Fetchable fieldValue fetchModel, Table fetchModel@@ -240,7 +240,7 @@ ?modelContext :: ModelContext, UpdateField field model (Include field model) (Maybe fieldValue) (Maybe (FetchResult fieldValue fetchModel)), HasField field model (Maybe fieldValue),- PG.FromRow fetchModel,+ FromRowHasql fetchModel, KnownSymbol (GetTableName fetchModel), Fetchable fieldValue fetchModel, Table fetchModel
@@ -31,17 +31,20 @@ import Network.Minio import qualified Data.Conduit.Binary as Conduit import qualified Network.Wai.Parse as Wai+import Network.Wai (Request) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import qualified Data.TMap as TMap import qualified Data.Text as Text import qualified Data.ByteString.Lazy as LBS-import qualified System.Directory as Directory+import qualified System.Directory.OsPath as Directory import qualified Control.Exception.Safe as Exception+import System.OsPath (encodeUtf) import qualified Network.Wreq as Wreq import Control.Lens hiding ((|>), set) import qualified Network.Mime as Mime+import qualified Network.HTTP.Types.URI as URI -- | Uploads a file to a directory in the storage --@@ -105,7 +108,8 @@ url <- case storage of StaticDirStorage { directory } -> do let destPath :: Text = directory <> objectPath- Directory.createDirectoryIfMissing True (cs $ directory <> options.directory)+ dirOsPath <- encodeUtf (cs $ directory <> options.directory)+ Directory.createDirectoryIfMissing True dirOsPath fileInfo |> (.fileContent)@@ -310,7 +314,9 @@ pure record contentDispositionAttachmentAndFileName :: Wai.FileInfo LByteString -> IO (Maybe Text)-contentDispositionAttachmentAndFileName fileInfo = pure (Just ("attachment; filename=\"" <> cs (fileInfo.fileName) <> "\""))+contentDispositionAttachmentAndFileName fileInfo =+ let encodedFileName = cs (URI.urlEncode False (fileInfo.fileName))+ in pure (Just ("attachment; filename*=UTF-8''" <> encodedFileName)) -- | Saves an upload to the storage and sets the record attribute to the url. --@@ -332,6 +338,7 @@ -- uploadToStorageWithOptions :: forall (fieldName :: Symbol) record (tableName :: Symbol). ( ?context :: ControllerContext+ , ?request :: Request , SetField fieldName record (Maybe Text) , KnownSymbol fieldName , HasField "id" record (ModelSupport.Id (ModelSupport.NormalizeModel record))@@ -381,6 +388,7 @@ -- uploadToStorage :: forall (fieldName :: Symbol) record (tableName :: Symbol). ( ?context :: ControllerContext+ , ?request :: Request , SetField fieldName record (Maybe Text) , KnownSymbol fieldName , HasField "id" record (ModelSupport.Id (ModelSupport.NormalizeModel record))@@ -409,8 +417,8 @@ removeFileFromStorage StoredFile { path, url } = do case storage of StaticDirStorage { directory } -> do- let fullPath :: String = cs $ directory <> path- Directory.removeFile fullPath+ fullOsPath <- encodeUtf (cs $ directory <> path)+ Directory.removeFile fullOsPath pure $ Right () S3Storage { connectInfo, bucket} -> do runMinio connectInfo do
@@ -1,65 +0,0 @@-{-|-Module: IHP.FileStorage.Preprocessor.ImageMagick-Description: Preprocessor for images. Requires that you add @imagemagick@ to the @otherDeps@ inside the project's @default.nix.-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.FileStorage.Preprocessor.ImageMagick-( applyImageMagick-) where--import IHP.Prelude--import qualified Network.Wai.Parse as Wai-import qualified Data.ByteString.Lazy as LBS-import qualified System.IO.Temp as Temp-import qualified System.Process as Process----- | Converts the image to the specified output format and applies specified image magick transforms------ __Example:__ Transform an uploaded image to a PNG file, resize it to 512x512 and strip meta data------ > let uploadLogo = uploadToStorageWithOptions $ def--- > { preprocess = applyImageMagick "png" ["-resize", "512x512^", "-gravity", "north", "-extent" , "512x512", "-quality", "100%", "-strip"] }--- >--- > company <- fetch companyId--- > company--- > |> fill @'["name"]--- > |> uploadLogo #logoUrl--- > >>= ifValid \case--- > Left company -> render EditView { .. }--- > Right company -> do--- > company <- company |> updateRecord--- > redirectTo EditCompanyAction { .. }--------- __Example:__ Transform an uploaded image to a JPEG file and strip meta data------ > let uploadLogo = uploadToStorageWithOptions $ def--- > { preprocess = applyImageMagick "jpg" ["-strip"] }--- >--- > company <- fetch companyId--- > company--- > |> fill @'["name"]--- > |> uploadLogo #logoUrl--- > >>= ifValid \case--- > Left company -> render EditView { .. }--- > Right company -> do--- > company <- company |> updateRecord--- > redirectTo EditCompanyAction { .. }----applyImageMagick :: Text -> [String] -> Wai.FileInfo LByteString -> IO (Wai.FileInfo LByteString)-applyImageMagick convertTo otherParams fileInfo = do- Temp.withTempDirectory "/tmp" "ihp-upload" $ \tempPath -> do- let tempFilePath = tempPath <> "/image"- let convertedFilePath = tempPath <> "/converted." <> cs convertTo-- fileInfo- |> (.fileContent)- |> LBS.writeFile tempFilePath-- let params :: [String] = [tempFilePath] <> otherParams <> [convertedFilePath]- Process.callProcess "magick" params-- newContent <- LBS.readFile convertedFilePath- pure fileInfo { Wai.fileContent = newContent }
@@ -0,0 +1,95 @@+{-|+Module: IHP.FlashMessages+Description: Success and error messages for your application+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.FlashMessages where++import Prelude+import Data.Text (Text)+import Data.Maybe (fromMaybe)+import IHP.Controller.Session+import qualified Network.Wai.Middleware.FlashMessages as FlashMessages+import Network.Wai.Middleware.FlashMessages (FlashMessage (..))+import Network.Wai+import qualified Data.Vault.Lazy as Vault+import System.IO.Unsafe (unsafePerformIO)+import qualified Text.Blaze.Html5 as Html5+import IHP.ViewSupport+import IHP.View.Types++-- | Sets a flash messages. This is shown to the user when the next view is rendered.+--+-- Will be rendered in a bootstrap alert, with the @alert-success@ styling.+-- Take a look at https://getbootstrap.com/docs/4.5/components/alerts/ for how this will look like.+--+-- This requires 'IHP.ViewSupport.renderFlashMessages' to be placed somewhere in the layout or page of the next view.+-- For example:+--+-- > myLayout view = [hsx|+-- > {renderFlashMessages}+-- > <main>{view}</main>+-- > |]+setSuccessMessage :: (?request :: Request) => Text -> IO ()+setSuccessMessage = FlashMessages.setSuccessMessage sessionVaultKey ?request++-- | Sets a flash messages. This is shown to the user when the next view is rendered.+--+-- Will be rendered in a bootstrap alert, with the @alert-danger@ styling.+-- Take a look at https://getbootstrap.com/docs/4.5/components/alerts/ for how this will look like.+--+-- This requires 'IHP.ViewSupport.renderFlashMessages' to be placed somewhere in the layout or page of the next view.+-- For example:+--+-- > myLayout view = [hsx|+-- > {renderFlashMessages}+-- > <main>{view}</main>+-- > |]+setErrorMessage :: (?request :: Request) => Text -> IO ()+setErrorMessage = FlashMessages.setErrorMessage sessionVaultKey ?request++-- | Returns the flash message currently set+getSuccessMessage :: (?request :: Request) => IO (Maybe Text)+getSuccessMessage = FlashMessages.getSuccessMessage sessionVaultKey ?request++-- | Removes the current flash message+clearSuccessMessage :: (?request :: Request) => IO ()+clearSuccessMessage = FlashMessages.clearSuccessMessage sessionVaultKey ?request++flashVaultKey :: Vault.Key [FlashMessage]+flashVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE flashVaultKey #-}++consumeFlashMessagesMiddleware = FlashMessages.consumeFlashMessagesMiddleware sessionVaultKey flashVaultKey++requestFlashMessages :: Request -> [FlashMessage]+requestFlashMessages request =+ fromMaybe [] $ FlashMessages.requestFlashMessages flashVaultKey request++-- | Displays the flash messages for the current request.+--+-- You can add a flash message to the next request by calling 'IHP.FlashMessages.setSuccessMessage' or 'IHP.FlashMessages.setErrorMessage':+--+-- > action CreateProjectAction = do+-- > ...+-- > setSuccessMessage "Your project has been created successfully"+-- > redirectTo ShowProjectAction { .. }+--+--+-- > action CreateTeamAction = do+-- > unless userOnPaidPlan do+-- > setErrorMessage "This requires you to be on the paid plan"+-- > redirectTo NewTeamAction+-- >+-- > ...+--+-- For success messages, the text message is wrapped in a @<div class="alert alert-success">...</div>@, which is automatically styled by bootstrap.+-- Errors flash messages are wraped in @<div class="alert alert-danger">...</div>@.+renderFlashMessages :: (?request :: Request) => Html5.Html+renderFlashMessages = render theFlashMessages+ where+ render :: [FlashMessage] -> Html5.Html+ render = fromCSSFramework #styledFlashMessages++theFlashMessages :: (?request :: Request) => [FlashMessage]+theFlashMessages = requestFlashMessages ?request
@@ -1,70 +0,0 @@-{-|-Module: IHP.FlashMessages.ControllerFunctions-Description: Success and error messages for your application-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.FlashMessages.ControllerFunctions where--import IHP.Prelude-import IHP.FlashMessages.Types-import IHP.Controller.Context-import IHP.Controller.Session-import qualified Data.Maybe as Maybe---- | Sets a flash messages. This is shown to the user when the next view is rendered.------ Will be rendered in a bootstrap alert, with the @alert-success@ styling.--- Take a look at https://getbootstrap.com/docs/4.5/components/alerts/ for how this will look like.------ This requires 'IHP.ViewSupport.renderFlashMessages' to be placed somewhere in the layout or page of the next view.--- For example:------ > myLayout view = [hsx|--- > {renderFlashMessages}--- > <main>{view}</main>--- > |]-setSuccessMessage :: (?context :: ControllerContext) => Text -> IO ()-setSuccessMessage = setSession successMessageKey---- | Sets a flash messages. This is shown to the user when the next view is rendered.------ Will be rendered in a bootstrap alert, with the @alert-danger@ styling.--- Take a look at https://getbootstrap.com/docs/4.5/components/alerts/ for how this will look like.------ This requires 'IHP.ViewSupport.renderFlashMessages' to be placed somewhere in the layout or page of the next view.--- For example:------ > myLayout view = [hsx|--- > {renderFlashMessages}--- > <main>{view}</main>--- > |]-setErrorMessage :: (?context :: ControllerContext) => Text -> IO ()-setErrorMessage = setSession errorMessageKey---- | Returns the flash message currently set-getSuccessMessage :: (?context :: ControllerContext) => IO (Maybe Text)-getSuccessMessage = getSession successMessageKey---- | Removes the current flash message-clearSuccessMessage :: (?context :: ControllerContext) => IO ()-clearSuccessMessage = setSession successMessageKey ("" :: Text)---- Returns a list of all flash messages which need to be displayed to the user------ Then clears the flash messages so they won't be displayed again.-getAndClearFlashMessages :: (?context :: ControllerContext) => IO [FlashMessage]-getAndClearFlashMessages = do- successMessage <- getSessionAndClear successMessageKey- errorMessage <- getSessionAndClear errorMessageKey- pure $ Maybe.catMaybes ((fmap SuccessFlashMessage successMessage):(fmap ErrorFlashMessage errorMessage):[])--successMessageKey :: ByteString-successMessageKey = "flashSuccessMessage"--errorMessageKey :: ByteString-errorMessageKey = "flashErrorMessage"--initFlashMessages :: (?context :: ControllerContext) => IO ()-initFlashMessages = do- flashMessages <- getAndClearFlashMessages- putContext flashMessages
@@ -1,13 +0,0 @@-{-|-Module: IHP.FlashMessages.Types-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.FlashMessages.Types-( FlashMessage (..)-) where--import IHP.Prelude--data FlashMessage- = SuccessFlashMessage !Text- | ErrorFlashMessage !Text
@@ -1,41 +0,0 @@-{-|-Module: IHP.FlashMessages.ViewFunctions-Description: Success and error messages for your application-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.FlashMessages.ViewFunctions where--import IHP.FlashMessages.Types-import qualified Text.Blaze.Html5 as Html5-import IHP.ViewSupport-import IHP.View.Types-import IHP.Controller.Context----- | Displays the flash messages for the current request.------ You can add a flash message to the next request by calling 'IHP.FlashMessages.ControllerFunctions.setSuccessMessage' or 'IHP.FlashMessages.ControllerFunctions.setErrorMessage':------ > action CreateProjectAction = do--- > ...--- > setSuccessMessage "Your project has been created successfully"--- > redirectTo ShowProjectAction { .. }--------- > action CreateTeamAction = do--- > unless userOnPaidPlan do--- > setErrorMessage "This requires you to be on the paid plan"--- > redirectTo NewTeamAction--- >--- > ...------ For success messages, the text message is wrapped in a @<div class="alert alert-success">...</div>@, which is automatically styled by bootstrap.--- Errors flash messages are wraped in @<div class="alert alert-danger">...</div>@.-renderFlashMessages :: (?context :: ControllerContext) => Html5.Html-renderFlashMessages = render flashMessages- where- flashMessages :: [FlashMessage]- flashMessages = fromFrozenContext-- render :: [FlashMessage] -> Html5.Html- render = fromCSSFramework #styledFlashMessages
@@ -1,89 +1,53 @@ {-# LANGUAGE ConstraintKinds #-}-module IHP.FrameworkConfig where+module IHP.FrameworkConfig+( -- * Re-exports from IHP.FrameworkConfig.Types+ module IHP.FrameworkConfig.Types+ -- * Configuration helpers+, option+, addInitializer+, findOption+, findOptionOrNothing+, ihpDefaultConfig+, buildFrameworkConfig+, defaultIHPSessionCookie+, RootApplication (..)+, defaultPort+, defaultDatabaseUrl+, defaultLoggerForEnv+, isEnvironment+, isDevelopment+, isProduction+, defaultCorsResourcePolicy+, withFrameworkConfig+, configIO+, ExceptionWithCallStack (..)+) where import IHP.Prelude-import System.Directory (getCurrentDirectory)+import IHP.FrameworkConfig.Types+import qualified System.Directory.OsPath as Directory import IHP.Environment+import System.OsPath (decodeUtf) import qualified Data.Text as Text import qualified Network.Wai.Middleware.RequestLogger as RequestLogger import qualified Web.Cookie as Cookie-import IHP.Mail.Types import qualified Control.Monad.Trans.State.Strict as State import qualified Data.TMap as TMap import qualified Data.Typeable as Typeable import IHP.View.Types-import IHP.View.CSSFramework+import IHP.View.CSSFramework.Bootstrap (bootstrap) import IHP.Log.Types import IHP.Log (makeRequestLogger, defaultRequestLogger)-import Network.Wai import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Middleware.Cors as Cors import qualified Network.Wai.Parse as WaiParse import qualified Control.Exception as Exception-import IHP.ModelSupport import IHP.EnvVar import qualified Prelude import qualified GHC.Stack as Stack -import qualified Control.Concurrent as Concurrent -newtype AppHostname = AppHostname Text-newtype AppPort = AppPort Int-newtype BaseUrl = BaseUrl Text---- | Provides IHP with a middleware to log requests and responses.------ By default this uses the RequestLogger middleware from wai-extra. Take a look at the wai-extra--- documentation when you want to customize the request logging.------ See https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html--------- Set @requestLoggerMiddleware = \application -> application@ to disable request logging.-newtype RequestLoggerMiddleware = RequestLoggerMiddleware Middleware---- | Provides the default settings for the session cookie.------ - Max Age: 30 days--- - Same Site Policy: Lax--- - HttpOnly (no access through JS)--- - secure, when baseUrl is a https url------ Override this to set e.g. a custom max age or change the default same site policy.------ __Example: Set max age to 90 days__--- > sessionCookie = defaultIHPSessionCookie { Cookie.setCookieMaxAge = Just (fromIntegral (60 * 60 * 24 * 90)) }-newtype SessionCookie = SessionCookie Cookie.SetCookie---- | How long db connection are kept alive inside the connecton pool when they're idle-newtype DBPoolIdleTime = DBPoolIdleTime NominalDiffTime---- | Max number of db connections the connection pool can open to the database-newtype DBPoolMaxConnections = DBPoolMaxConnections Int--newtype DatabaseUrl = DatabaseUrl ByteString--type ConfigBuilder = State.StateT TMap.TMap IO ()---- | Interface for exception tracking services such as sentry-newtype ExceptionTracker = ExceptionTracker { onException :: Maybe Request -> SomeException -> IO () }---- | Typically "http://localhost:8001", Url where the IDE is running-newtype IdeBaseUrl = IdeBaseUrl Text---- | Postgres role to be used for making queries with Row level security enabled-newtype RLSAuthenticatedRole = RLSAuthenticatedRole Text--newtype AssetVersion = AssetVersion Text--newtype CustomMiddleware = CustomMiddleware Middleware--newtype DataSyncMaxSubscriptionsPerConnection = DataSyncMaxSubscriptionsPerConnection Int-newtype DataSyncMaxTransactionsPerConnection = DataSyncMaxTransactionsPerConnection Int--newtype Initializer = Initializer { onStartup :: (?context :: FrameworkConfig, ?modelContext :: ModelContext) => IO () }- -- | Puts an option into the current configuration -- -- In case an option already exists with the same type, it will not be overriden:@@ -147,21 +111,9 @@ option $ defaultCorsResourcePolicy - option $ Sendmail- databaseUrl <- configIO defaultDatabaseUrl option $ DatabaseUrl databaseUrl- option $ DBPoolIdleTime $- case environment of- Development -> 2- Production -> 60-- -- poolMaxResources must not be smaller than numStripes- -- https://github.com/digitallyinduced/ihp/issues/1959- numCapabilities <- configIO Concurrent.getNumCapabilities- option $ DBPoolMaxConnections (max numCapabilities 20)- (AppPort port) <- findOption @AppPort -- The IHP_BASEURL env var can override the hardcoded base url in Config.hs@@ -186,8 +138,6 @@ rlsAuthenticatedRole <- envOrDefault "IHP_RLS_AUTHENTICATED_ROLE" "ihp_authenticated" option $ RLSAuthenticatedRole rlsAuthenticatedRole - initAssetVersion- dataSyncMaxSubscriptionsPerConnection <- envOrDefault "IHP_DATASYNC_MAX_SUBSCRIPTIONS_PER_CONNECTION" 128 dataSyncMaxTransactionsPerConnection <- envOrDefault "IHP_DATASYNC_MAX_TRANSACTIONS_PER_CONNECTION" 10 option $ DataSyncMaxSubscriptionsPerConnection dataSyncMaxSubscriptionsPerConnection@@ -205,12 +155,6 @@ envStringToValue "FromSocket" = Right RequestLogger.FromSocket envStringToValue otherwise = Left "Expected 'FromHeader' or 'FromSocket'" -initAssetVersion :: ConfigBuilder-initAssetVersion = do- ihpAssetVersion <- envOrNothing "IHP_ASSET_VERSION"- let assetVersion = fromMaybe "dev" ihpAssetVersion- option (AssetVersion assetVersion)- findOption :: forall option. Typeable option => State.StateT TMap.TMap IO option findOption = fromMaybe (error optionNotFoundErrorMessage) <$> findOptionOrNothing @option where@@ -234,9 +178,6 @@ (BaseUrl baseUrl) <- findOption @BaseUrl (RequestLoggerMiddleware requestLoggerMiddleware) <- findOption @RequestLoggerMiddleware (SessionCookie sessionCookie) <- findOption @SessionCookie- mailServer <- findOption @MailServer- (DBPoolIdleTime dbPoolIdleTime) <- findOption @DBPoolIdleTime- (DBPoolMaxConnections dbPoolMaxConnections) <- findOption @DBPoolMaxConnections (DatabaseUrl databaseUrl) <- findOption @DatabaseUrl cssFramework <- findOption @CSSFramework logger <- findOption @Logger@@ -245,8 +186,6 @@ parseRequestBodyOptions <- findOption @WaiParse.ParseRequestBodyOptions (IdeBaseUrl ideBaseUrl) <- findOption @IdeBaseUrl (RLSAuthenticatedRole rlsAuthenticatedRole) <- findOption @RLSAuthenticatedRole- (AssetVersion assetVersion) <- findOption @AssetVersion- assetBaseUrl <- envOrNothing "IHP_ASSET_BASEURL" customMiddleware <- findOption @CustomMiddleware initializers <- fromMaybe [] <$> findOptionOrNothing @[Initializer] @@ -260,167 +199,6 @@ pure frameworkConfig {-# INLINABLE buildFrameworkConfig #-} -data FrameworkConfig = FrameworkConfig- { appHostname :: !Text- , environment :: !Environment- , appPort :: !Int- , baseUrl :: !Text-- -- | Provides IHP with a middleware to log requests and responses.- --- -- By default this uses the RequestLogger middleware from wai-extra. Take a look at the wai-extra- -- documentation when you want to customize the request logging.- --- -- See https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html- --- --- -- Set @requestLoggerMiddleware = \application -> application@ to disable request logging.- , requestLoggerMiddleware :: !Middleware-- -- | Provides the default settings for the session cookie.- --- -- - Max Age: 30 days- -- - Same Site Policy: Lax- -- - HttpOnly (no access through JS)- -- - secure, when baseUrl is a https url- --- -- Override this to set e.g. a custom max age or change the default same site policy.- --- -- __Example: Set max age to 90 days__- -- > sessionCookie = defaultIHPSessionCookie { Cookie.setCookieMaxAge = Just (fromIntegral (60 * 60 * 24 * 90)) }- , sessionCookie :: !Cookie.SetCookie-- , mailServer :: !MailServer-- , databaseUrl :: !ByteString- -- | How long db connection are kept alive inside the connecton pool when they're idle- , dbPoolIdleTime :: !NominalDiffTime-- -- | Max number of db connections the connection pool can open to the database- , dbPoolMaxConnections :: !Int-- -- | Bootstrap 4 by default- --- -- Override this if you use a CSS framework that is not bootstrap- , cssFramework :: !CSSFramework- , logger :: !Logger- , exceptionTracker :: !ExceptionTracker-- -- | Custom 'option's from @Config.hs@ are stored here- --- -- To access a custom option here, first set it up inside @Config.hs@. This example- -- reads a string from a env variable on app startup and makes it available to the app- -- by saving it into the application context:- --- -- > -- Config.hs:- -- >- -- > newtype RedisUrl = RedisUrl String- -- >- -- > config :: ConfigBuilder- -- > config = do- -- > option Development- -- > option (AppHostname "localhost")- -- >- -- > redisUrl <- env "REDIS_URL"- -- > option (RedisUrl redisUrl)- --- -- This redis url can be access from all IHP entrypoints using the ?applicationContext that is in scope:- --- -- > import qualified Data.TMap as TMap- -- > import Config -- For accessing the RedisUrl data type- -- >- -- > action MyAction = do- -- > let appConfig = ?context.frameworkConfig.appConfig- -- > let (RedisUrl redisUrl) = appConfig- -- > |> TMap.lookup @RedisUrl- -- > |> fromMaybe (error "Could not find RedisUrl in config")- -- >- , appConfig :: !TMap.TMap-- -- | Configures CORS headers for the application. By default this is set to 'Nothing', and the server will not respond with any CORS headers- --- -- You can provide a custom CORS policy in @Config.hs@:- --- -- > -- Config.hs- -- > import qualified Network.Wai.Middleware.Cors as Cors- -- >- -- > config :: ConfigBuilder- -- > config = do- -- > option Development- -- > option (AppHostname "localhost")- -- >- -- > option Cors.simpleCorsResourcePolicy- -- >- --- -- Take a look at the documentation of wai-cors https://hackage.haskell.org/package/wai-cors-0.2.7/docs/Network-Wai-Middleware-Cors.html for understanding what @simpleCorsResourcePolicy@ is doing- --- -- You can specify CORS origins like this:- --- -- > -- Config.hs- -- > import qualified Network.Wai.Middleware.Cors as Cors- -- >- -- > config :: ConfigBuilder- -- > config = do- -- > option Development- -- > option (AppHostname "localhost")- -- >- -- > -- The boolean True specifies if credentials are allowed for the request. You still need to set withCredentials on your XmlHttpRequest- -- > option Cors.simpleCorsResourcePolicy { Cors.corsOrigins = Just (["localhost"], True) }- -- >- , corsResourcePolicy :: !(Maybe Cors.CorsResourcePolicy)-- -- | Configures the limits for request parameters, uploaded files, maximum number of headers etc.- --- -- IHP is using 'Network.Wai.Parse.parseRequestBodyEx' for parsing the HTTP request. By default it applies certain limits- -- to avoid a single request overloading the server.- --- -- You can find the default limits here: https://hackage.haskell.org/package/wai-extra-3.1.6/docs/Network-Wai-Parse.html#v:defaultParseRequestBodyOptions- --- -- You can override the default limits like this:- --- -- > -- Config.hs- -- > import qualified Network.Wai.Parse as WaiParse- -- >- -- > config :: ConfigBuilder- -- > config = do- -- > option Development- -- > option (AppHostname "localhost")- -- >- -- > -- We extend the default options here- -- > option $ WaiParse.defaultParseRequestBodyOptions- -- > |> WaiParse.setMaxRequestNumFiles 20 -- Increase count of allowed files per request- -- >- , parseRequestBodyOptions :: !WaiParse.ParseRequestBodyOptions-- -- | Used by the dev server. This field cannot be strict.- , ideBaseUrl :: Text-- -- | See IHP.DataSync.Role- , rlsAuthenticatedRole :: !Text-- -- | The asset version is used for cache busting- --- -- If you deploy IHP on your own, you should provide the IHP_ASSET_VERSION- -- env variable with e.g. the git commit hash of the production build.- --- -- If IHP cannot figure out an asset version, it will fallback to the static- -- string @"dev"@.- --- , assetVersion :: !Text-- -- | Base URL used by the 'assetPath' helper. Useful to move your static files to a CDN- , assetBaseUrl :: !(Maybe Text)-- -- | User provided WAI middleware that is run after IHP's middleware stack.- , customMiddleware :: !CustomMiddleware- , initializers :: ![Initializer]-}--instance HasField "frameworkConfig" FrameworkConfig FrameworkConfig where- getField frameworkConfig = frameworkConfig--type ConfigProvider context = HasField "frameworkConfig" context FrameworkConfig- -- | Returns the default IHP session cookie configuration. Useful when you want to override the default settings in 'sessionCookie' defaultIHPSessionCookie :: Text -> Cookie.SetCookie defaultIHPSessionCookie baseUrl = def@@ -438,7 +216,8 @@ defaultDatabaseUrl :: HasCallStack => IO ByteString defaultDatabaseUrl = do- currentDirectory <- getCurrentDirectory+ currentDirectoryOsPath <- Directory.getCurrentDirectory+ currentDirectory <- decodeUtf currentDirectoryOsPath let defaultDatabaseUrl = "postgresql:///app?host=" <> cs currentDirectory <> "/build/db" envOrDefault "DATABASE_URL" defaultDatabaseUrl @@ -488,12 +267,6 @@ -- withFrameworkConfig :: ConfigBuilder -> (FrameworkConfig -> IO result) -> IO result withFrameworkConfig configBuilder = Exception.bracket (buildFrameworkConfig configBuilder) (\frameworkConfig -> frameworkConfig.logger.cleanup)--initModelContext :: FrameworkConfig -> IO ModelContext-initModelContext FrameworkConfig { environment, dbPoolIdleTime, dbPoolMaxConnections, databaseUrl, logger } = do- let isDevelopment = environment == Development- modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections databaseUrl logger- pure modelContext -- | Wraps an Exception thrown during the config process, but adds a CallStack --
@@ -0,0 +1,161 @@+{-# LANGUAGE ConstraintKinds #-}+{-|+Module: IHP.FrameworkConfig.Types+Description: Core types for framework configuration+Copyright: (c) digitally induced GmbH, 2020++This module contains the core types for IHP's framework configuration.+It's designed to be lightweight and avoid Template Haskell dependencies,+allowing modules that only need the types to compile faster.++For the full configuration API including defaults, use 'IHP.FrameworkConfig'.+-}+module IHP.FrameworkConfig.Types+( AppHostname (..)+, AppPort (..)+, BaseUrl (..)+, RequestLoggerMiddleware (..)+, SessionCookie (..)+, DatabaseUrl (..)+, ConfigBuilder+, ExceptionTracker (..)+, IdeBaseUrl (..)+, RLSAuthenticatedRole (..)+, AssetVersion (..)+, CustomMiddleware (..)+, DataSyncMaxSubscriptionsPerConnection (..)+, DataSyncMaxTransactionsPerConnection (..)+, Initializer (..)+, FrameworkConfig (..)+, ConfigProvider+) where++import Prelude+import Data.ByteString (ByteString)+import Data.Text (Text)+import Control.Exception (SomeException)+import GHC.Records (HasField(..))+import qualified Control.Monad.Trans.State.Strict as State+import qualified Data.TMap as TMap+import qualified Web.Cookie as Cookie+import qualified Network.Wai.Middleware.Cors as Cors+import qualified Network.Wai.Parse as WaiParse+import Network.Wai (Middleware, Request)+import IHP.Environment (Environment)+import IHP.View.Types (CSSFramework)+import IHP.Log.Types (Logger)+import IHP.ModelSupport.Types (ModelContext)++newtype AppHostname = AppHostname Text+newtype AppPort = AppPort Int+newtype BaseUrl = BaseUrl Text++-- | Provides IHP with a middleware to log requests and responses.+--+-- By default this uses the RequestLogger middleware from wai-extra. Take a look at the wai-extra+-- documentation when you want to customize the request logging.+--+-- See https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html+--+--+-- Set @requestLoggerMiddleware = \application -> application@ to disable request logging.+newtype RequestLoggerMiddleware = RequestLoggerMiddleware Middleware++-- | Provides the default settings for the session cookie.+--+-- - Max Age: 30 days+-- - Same Site Policy: Lax+-- - HttpOnly (no access through JS)+-- - secure, when baseUrl is a https url+--+-- Override this to set e.g. a custom max age or change the default same site policy.+--+-- __Example: Set max age to 90 days__+-- > sessionCookie = defaultIHPSessionCookie { Cookie.setCookieMaxAge = Just (fromIntegral (60 * 60 * 24 * 90)) }+newtype SessionCookie = SessionCookie Cookie.SetCookie++newtype DatabaseUrl = DatabaseUrl ByteString++type ConfigBuilder = State.StateT TMap.TMap IO ()++-- | Interface for exception tracking services such as sentry+newtype ExceptionTracker = ExceptionTracker { onException :: Maybe Request -> SomeException -> IO () }++-- | Typically "http://localhost:8001", Url where the IDE is running+newtype IdeBaseUrl = IdeBaseUrl Text++-- | Postgres role to be used for making queries with Row level security enabled+newtype RLSAuthenticatedRole = RLSAuthenticatedRole Text++newtype AssetVersion = AssetVersion Text++newtype CustomMiddleware = CustomMiddleware Middleware++newtype DataSyncMaxSubscriptionsPerConnection = DataSyncMaxSubscriptionsPerConnection Int+newtype DataSyncMaxTransactionsPerConnection = DataSyncMaxTransactionsPerConnection Int++newtype Initializer = Initializer { onStartup :: (?context :: FrameworkConfig, ?modelContext :: ModelContext) => IO () }++data FrameworkConfig = FrameworkConfig+ { appHostname :: !Text+ , environment :: !Environment+ , appPort :: !Int+ , baseUrl :: !Text++ -- | Provides IHP with a middleware to log requests and responses.+ --+ -- By default this uses the RequestLogger middleware from wai-extra. Take a look at the wai-extra+ -- documentation when you want to customize the request logging.+ --+ -- See https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html+ --+ --+ -- Set @requestLoggerMiddleware = \application -> application@ to disable request logging.+ , requestLoggerMiddleware :: !Middleware++ -- | Provides the default settings for the session cookie.+ --+ -- - Max Age: 30 days+ -- - Same Site Policy: Lax+ -- - HttpOnly (no access through JS)+ -- - secure, when baseUrl is a https url+ --+ -- Override this to set e.g. a custom max age or change the default same site policy.+ --+ -- __Example: Set max age to 90 days__+ -- > sessionCookie = defaultIHPSessionCookie { Cookie.setCookieMaxAge = Just (fromIntegral (60 * 60 * 24 * 90)) }+ , sessionCookie :: !Cookie.SetCookie++ , databaseUrl :: !ByteString++ -- | Bootstrap 4 by default+ --+ -- Override this if you use a CSS framework that is not bootstrap+ , cssFramework :: !CSSFramework+ , logger :: !Logger+ , exceptionTracker :: !ExceptionTracker++ -- | Custom 'option's from @Config.hs@ are stored here+ , appConfig :: !TMap.TMap++ -- | Configures CORS headers for the application+ , corsResourcePolicy :: !(Maybe Cors.CorsResourcePolicy)++ -- | Configures the limits for request parameters, uploaded files, maximum number of headers etc.+ , parseRequestBodyOptions :: !WaiParse.ParseRequestBodyOptions++ -- | Used by the dev server. This field cannot be strict.+ , ideBaseUrl :: Text++ -- | See IHP.DataSync.Role+ , rlsAuthenticatedRole :: !Text++ -- | User provided WAI middleware that is run after IHP's middleware stack.+ , customMiddleware :: !CustomMiddleware+ , initializers :: ![Initializer]+ }++instance HasField "frameworkConfig" FrameworkConfig FrameworkConfig where+ getField frameworkConfig = frameworkConfig++type ConfigProvider context = HasField "frameworkConfig" context FrameworkConfig
@@ -1,4 +1,4 @@-{-# LANGUAGE TypeFamilies, DataKinds, MultiParamTypeClasses, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, IncoherentInstances, AllowAmbiguousTypes, FunctionalDependencies #-}+{-# LANGUAGE TypeFamilies, DataKinds, MultiParamTypeClasses, PolyKinds, TypeApplications, ScopedTypeVariables, TypeOperators, GADTs, StandaloneDeriving, IncoherentInstances, AllowAmbiguousTypes, FunctionalDependencies #-} {-| Module: IHP.HaskellSupport@@ -38,14 +38,22 @@ , allEnumValues ) where -import ClassyPrelude+import Control.Monad (when, unless, forM_)+import Data.Functor ((<&>))+import Data.String (IsString(..))+import Data.MonoTraversable (MonoFoldable, Element, oelem, oforM_)+import Data.Time.Clock (UTCTime(..), getCurrentTime)+import Data.Time.Calendar (Day, toGregorian)+import Data.Map (Map)+import Data.ByteString (ByteString)+import Data.Text (Text)+import Prelude import qualified Data.Default import qualified Data.UUID as UUID import Data.Proxy import qualified Data.Time import GHC.TypeLits import GHC.OverloadedLabels-import qualified GHC.Records as Record import qualified Data.Attoparsec.ByteString.Char8 as Attoparsec import Data.String.Conversions (cs, ConvertibleStrings (..)) import qualified Debug.Trace@@ -54,10 +62,7 @@ import qualified Data.ByteString.Char8 as ByteString import qualified Data.Aeson.Key as Aeson ---(|>) :: a -> f -> f a-infixl 8 |>-a |> f = f a-{-# INLINE (|>) #-}+import IHP.Record ((|>), get, set, setJust, setMaybe, modify, modifyJust, SetField(..), UpdateField(..), incrementField, decrementField, CopyFields(..)) infixl 8 |>> a |>> b = a <&> b@@ -112,7 +117,7 @@ -- >>> "Hello" |> includes 'H' -- True includes :: (MonoFoldable container, Eq (Element container)) => Element container -> container -> Bool-includes = elem+includes = oelem {-# INLINE includes #-} instance Data.Default.Default UUID.UUID where@@ -123,133 +128,6 @@ fromLabel = Proxy @name' {-# INLINE fromLabel #-} --- | Returns the field value for a field name------ __Example:__------ > data Project = Project { name :: Text, isPublic :: Bool }--- >--- > let project = Project { name = "Hello World", isPublic = False }------ >>> project.name--- "Hello World"------ >>> project.isPublic--- False-get :: forall model name value. (KnownSymbol name, Record.HasField name model value) => Proxy name -> model -> value-get _ record = Record.getField @name record-{-# INLINE get #-}---- | Sets a field of a record and returns the new record.------ __Example:__------ > data Project = Project { name :: Text, isPublic :: Bool }--- >--- > let project = Project { name = "Hello World", isPublic = False }------ >>> set #name "New Name" project--- Project { name = "New Name", isPublic = False }------ >>> set #isPublic True project--- Project { name = "Hello World", isPublic = True }-set :: forall model name value. (KnownSymbol name, SetField name model value) => Proxy name -> value -> model -> model-set name value record = setField @name value record-{-# INLINE set #-}----- | Like 'set' but doesn't set the value if it's 'Nothing'. Useful when you update NULL values--- | e.g. via a cron job and don't want to lose that work on subsequent updates.------ __Example:__------ > data Project = Project { name :: Maybe Text }--- >--- > let project = Project { name = Nothing }------ >>> setMaybe #name (Just "New Name") project--- Project { name = Just "New Name" }------ >>> setMaybe #name Nothing project--- Project { name = Just "New Name" } -- previous value is kept----setMaybe :: forall model name value. (KnownSymbol name, SetField name model (Maybe value)) => Proxy name -> Maybe value -> model -> model-setMaybe name value record = case value of- Just value -> setField @name (Just value) record- Nothing -> record-{-# INLINE setMaybe #-}----- | Like 'set' but wraps the value with a 'Just'. Useful when you want to set a 'Maybe' field------ __Example:__------ > data Project = Project { name :: Maybe Text }--- >--- > let project = Project { name = Nothing }------ >>> setJust #name "New Name" project--- Project { name = Just "New Name" }----setJust :: forall model name value. (KnownSymbol name, SetField name model (Maybe value)) => Proxy name -> value -> model -> model-setJust name value record = setField @name (Just value) record-{-# INLINE setJust #-}---modify :: forall model name value. (KnownSymbol name, Record.HasField name model value, SetField name model value) => Proxy name -> (value -> value) -> model -> model-modify _ updateFunction model = let value = Record.getField @name model in setField @name (updateFunction value) model-{-# INLINE modify #-}---- Like 'modify', but only modifies the value if it's not Nothing.------ __Example:__------ > let pauseDuration = now `diffUTCTime` pausedAt--- >--- > floorTimer <- floorTimer--- > |> modifyJust #startedAt (addUTCTime pauseDuration)--- > |> updateRecord----modifyJust :: forall model name value. (KnownSymbol name, Record.HasField name model (Maybe value), SetField name model (Maybe value)) => Proxy name -> (value -> value) -> model -> model-modifyJust _ updateFunction model = case Record.getField @name model of- Just value -> setField @name (Just (updateFunction value)) model- Nothing -> model-{-# INLINE modifyJust #-}---- | Plus @1@ on record field.------ __Example:__------ > data Project = Project { name :: Text, followersCount :: Int }--- >--- > let project = Project { name = "Hello World", followersCount = 0 }------ >>> project |> incrementField #followersCount--- Project { name = "Hello World", followersCount = 1 }-incrementField :: forall model name value. (KnownSymbol name, Record.HasField name model value, SetField name model value, Num value) => Proxy name -> model -> model-incrementField _ model = let value = Record.getField @name model in setField @name (value + 1) model-{-# INLINE incrementField #-}---- | Minus @1@ on a record field.------ __Example:__------ > data Project = Project { name :: Text, followersCount :: Int }--- >--- > let project = Project { name = "Hello World", followersCount = 1337 }------ >>> project |> decrementField #followersCount--- Project { name = "Hello World", followersCount = 1336 }-decrementField :: forall model name value. (KnownSymbol name, Record.HasField name model value, SetField name model value, Num value) => Proxy name -> model -> model-decrementField _ model = let value = Record.getField @name model in setField @name (value - 1) model-{-# INLINE decrementField #-}--class SetField (field :: GHC.TypeLits.Symbol) model value | field model -> value where- setField :: value -> model -> model--class Record.HasField field model value => UpdateField (field :: GHC.TypeLits.Symbol) model model' value value' | model model' value' -> value where- updateField :: value' -> model -> model'- utcTimeToYearMonthDay :: UTCTime -> (Integer, Int, Int) utcTimeToYearMonthDay = toGregorian . utctDay -- (year,month,day) @@ -279,7 +157,7 @@ -- > render = [hsx|{forEach users renderUser}|] -- forEach :: (MonoFoldable mono, Applicative m) => mono -> (Element mono -> m ()) -> m ()-forEach elements function = forM_ elements function+forEach elements function = oforM_ elements function {-# INLINE forEach #-} @@ -303,8 +181,8 @@ -- > -- > render = [hsx|{forEachWithIndex users renderUser}|] ---forEachWithIndex :: (Applicative m) => [a] -> ((Int, a) -> m ()) -> m ()-forEachWithIndex elements function = forM_ (ClassyPrelude.zip [0..] elements) function+forEachWithIndex :: (Monad m) => [a] -> ((Int, a) -> m ()) -> m ()+forEachWithIndex elements function = forM_ (zip [0..] elements) function {-# INLINE forEachWithIndex #-} -- | Parses a text to an int. Returns @Nothing@ on failure.@@ -391,39 +269,6 @@ fromString string = case UUID.fromString string of Just uuid -> uuid Nothing -> error ("Invalid UUID: " <> string)---class CopyFields (fields :: [Symbol]) destinationRecord sourceRecord where- -- | Provides the 'copyFields' function- --- -- Useful to rewrite getter-setter code like this:- --- -- > let newProject = newRecord @Project- -- > |> set #name (otherProject.name)- -- > |> set #isPublic (otherProject.isPublic)- -- > |> set #userId (otherProject.userId)- --- -- With 'copyFields' this can be written like this:- --- -- > let newProject = newRecord @Project- -- > |> copyFields @["name", "isPublic", "userId"] otherProject- --- copyFields :: sourceRecord -> destinationRecord -> destinationRecord--instance CopyFields ('[]) destinationRecord sourceRecord where- copyFields sourceRecord destinationRecord = destinationRecord- {-# INLINE copyFields #-}--instance (CopyFields rest destinationRecord sourceRecord- , KnownSymbol fieldName- , SetField fieldName destinationRecord fieldType- , Record.HasField fieldName sourceRecord fieldType- ) => CopyFields (fieldName:rest) destinationRecord sourceRecord where- copyFields sourceRecord destinationRecord =- destinationRecord- |> set (Proxy @fieldName) (Record.getField @fieldName sourceRecord)- |> copyFields @rest sourceRecord- {-# INLINE copyFields #-} -- | Returns a list of all values of an enum type --
@@ -0,0 +1,258 @@+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -Wno-orphans #-}+{-|+Module: IHP.Hasql.Encoders+Description: DefaultParamEncoder instances for common types+Copyright: (c) digitally induced GmbH, 2025++This module provides orphan 'DefaultParamEncoder' instances for types that+hasql-implicits doesn't support out of the box, most importantly 'Int'.++These instances are needed because hasql-implicits only provides instances for+fixed-width integer types ('Int16', 'Int32', 'Int64'), not Haskell's+platform-dependent 'Int'. Since most IHP applications use 'Int' for+integer columns, we provide these instances to make the transition seamless.+-}+module IHP.Hasql.Encoders+( ToSnippetParams(..)+, sqlToSnippet+) where++import Prelude+import Data.Int (Int64)+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS8+import Data.Text (Text)+import qualified Data.Text.Encoding as Text+import qualified Hasql.Encoders as Encoders+import Hasql.Implicits.Encoders (DefaultParamEncoder(..))+import qualified Hasql.DynamicStatements.Snippet as Snippet+import Hasql.DynamicStatements.Snippet (Snippet)+import Database.PostgreSQL.Simple (Only(..), (:.)(..))+import Data.Functor.Contravariant (contramap)+import Data.Functor.Contravariant.Divisible (divide)+import Data.Vector (Vector)+import IHP.ModelSupport.Types (Id'(..), PrimaryKey)+import Database.PostgreSQL.Simple.Types (Binary(..))+import qualified Hasql.Mapping.IsScalar as Mapping+import Hasql.PostgresqlTypes ()+import PostgresqlTypes.Point (Point)+import PostgresqlTypes.Polygon (Polygon)+import PostgresqlTypes.Inet (Inet)+import PostgresqlTypes.Interval (Interval)+import PostgresqlTypes.Tsvector (Tsvector)++-- | Encode 'Int' as PostgreSQL int8 (bigint)+--+-- This treats Haskell's 'Int' as 'Int64', which is safe on 64-bit platforms+-- (where 'Int' is 64 bits) and may truncate on 32-bit platforms (where 'Int'+-- is 32 bits but int8 can hold the full range).+instance DefaultParamEncoder Int where+ defaultParam = Encoders.nonNullable (contramap (fromIntegral :: Int -> Int64) Encoders.int8)++-- | Encode '[Int]' as PostgreSQL int8[] (bigint array)+instance DefaultParamEncoder [Int] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nonNullable (contramap (fromIntegral :: Int -> Int64) Encoders.int8)++-- | Encode 'Maybe Int' as nullable PostgreSQL int8+instance DefaultParamEncoder (Maybe Int) where+ defaultParam = Encoders.nullable (contramap (fromIntegral :: Int -> Int64) Encoders.int8)++-- | Encode '[Maybe Int]' as PostgreSQL int8[] with nullable elements+instance DefaultParamEncoder [Maybe Int] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nullable (contramap (fromIntegral :: Int -> Int64) Encoders.int8)++-- | Encode 'Vector Int' as PostgreSQL int8[] (bigint array)+instance DefaultParamEncoder (Vector Int) where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nonNullable (contramap (fromIntegral :: Int -> Int64) Encoders.int8)++-- | Encode 'Id' table' for tables with any primary key type that has an 'IsScalar' instance.+-- This covers UUID, Text, Int, and other primary key types.+instance Mapping.IsScalar (PrimaryKey table) => DefaultParamEncoder (Id' table) where+ defaultParam = Encoders.nonNullable (contramap (\(Id pk) -> pk) Mapping.encoder)++-- | Encode list of 'Id' table' for tables with any encodable primary key type.+-- Used by filterWhereIdIn for simple primary keys.+instance Mapping.IsScalar (PrimaryKey table) => DefaultParamEncoder [Id' table] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nonNullable (contramap (\(Id pk) -> pk) Mapping.encoder)++-- | Encode 'Maybe (Id' table)' for nullable foreign keys with any encodable primary key type.+instance Mapping.IsScalar (PrimaryKey table) => DefaultParamEncoder (Maybe (Id' table)) where+ defaultParam = Encoders.nullable (contramap (\(Id pk) -> pk) Mapping.encoder)++-- | Encode '[Maybe (Id' table)]' for filterWhereIn with nullable foreign keys.+instance Mapping.IsScalar (PrimaryKey table) => DefaultParamEncoder [Maybe (Id' table)] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nullable (contramap (\(Id pk) -> pk) Mapping.encoder)++-- | Encode '(Id' a, Id' b)' as PostgreSQL composite/record type+-- Used for composite primary keys with two Id columns of any scalar PK type+instance (Mapping.IsScalar (PrimaryKey a), Mapping.IsScalar (PrimaryKey b)) => DefaultParamEncoder (Id' a, Id' b) where+ defaultParam = Encoders.nonNullable $ Encoders.composite (Nothing :: Maybe Text) "" $+ divide (\(Id a, Id b) -> (a, b))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))++-- | Encode '[(Id' a, Id' b)]' as PostgreSQL array of composite types+-- Used by filterWhereIdIn for tables with two-column composite primary keys+instance (Mapping.IsScalar (PrimaryKey a), Mapping.IsScalar (PrimaryKey b)) => DefaultParamEncoder [(Id' a, Id' b)] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nonNullable $ Encoders.composite (Nothing :: Maybe Text) "" $+ divide (\(Id a, Id b) -> (a, b))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))++-- | Encode '(Id' a, Id' b, Id' c)' as PostgreSQL composite/record type+-- Used for composite primary keys with three Id columns of any scalar PK type+instance (Mapping.IsScalar (PrimaryKey a), Mapping.IsScalar (PrimaryKey b), Mapping.IsScalar (PrimaryKey c)) => DefaultParamEncoder (Id' a, Id' b, Id' c) where+ defaultParam = Encoders.nonNullable $ Encoders.composite (Nothing :: Maybe Text) "" $+ divide (\(Id a, Id b, Id c) -> (a, (b, c)))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (divide id (Encoders.field (Encoders.nonNullable Mapping.encoder)) (Encoders.field (Encoders.nonNullable Mapping.encoder)))++-- | Encode '[(Id' a, Id' b, Id' c)]' as PostgreSQL array of composite types+-- Used by filterWhereIdIn for tables with three-column composite primary keys+instance (Mapping.IsScalar (PrimaryKey a), Mapping.IsScalar (PrimaryKey b), Mapping.IsScalar (PrimaryKey c)) => DefaultParamEncoder [(Id' a, Id' b, Id' c)] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nonNullable $ Encoders.composite (Nothing :: Maybe Text) "" $+ divide (\(Id a, Id b, Id c) -> (a, (b, c)))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (divide id (Encoders.field (Encoders.nonNullable Mapping.encoder)) (Encoders.field (Encoders.nonNullable Mapping.encoder)))++-- | Encode '(Id' a, Id' b, Id' c, Id' d)' as PostgreSQL composite/record type+-- Used for composite primary keys with four Id columns of any scalar PK type+instance (Mapping.IsScalar (PrimaryKey a), Mapping.IsScalar (PrimaryKey b), Mapping.IsScalar (PrimaryKey c), Mapping.IsScalar (PrimaryKey d)) => DefaultParamEncoder (Id' a, Id' b, Id' c, Id' d) where+ defaultParam = Encoders.nonNullable $ Encoders.composite (Nothing :: Maybe Text) "" $+ divide (\(Id a, Id b, Id c, Id d) -> (a, (b, c, d)))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (divide (\(b, c, d) -> (b, (c, d)))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (divide id (Encoders.field (Encoders.nonNullable Mapping.encoder)) (Encoders.field (Encoders.nonNullable Mapping.encoder))))++-- | Encode '[(Id' a, Id' b, Id' c, Id' d)]' as PostgreSQL array of composite types+-- Used by filterWhereIdIn for tables with four-column composite primary keys+instance (Mapping.IsScalar (PrimaryKey a), Mapping.IsScalar (PrimaryKey b), Mapping.IsScalar (PrimaryKey c), Mapping.IsScalar (PrimaryKey d)) => DefaultParamEncoder [(Id' a, Id' b, Id' c, Id' d)] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nonNullable $ Encoders.composite (Nothing :: Maybe Text) "" $+ divide (\(Id a, Id b, Id c, Id d) -> (a, (b, c, d)))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (divide (\(b, c, d) -> (b, (c, d)))+ (Encoders.field (Encoders.nonNullable Mapping.encoder))+ (divide id (Encoders.field (Encoders.nonNullable Mapping.encoder)) (Encoders.field (Encoders.nonNullable Mapping.encoder))))++-- | Encode 'Binary ByteString' as PostgreSQL bytea+-- IHP wraps bytea columns in Binary, so we need to unwrap before encoding+instance DefaultParamEncoder (Binary ByteString) where+ defaultParam = Encoders.nonNullable (contramap (\(Binary bs) -> bs) Encoders.bytea)++-- | Encode 'Maybe (Binary ByteString)' as nullable PostgreSQL bytea+instance DefaultParamEncoder (Maybe (Binary ByteString)) where+ defaultParam = Encoders.nullable (contramap (\(Binary bs) -> bs) Encoders.bytea)++-- | Encode 'Integer' as PostgreSQL int8 (bigint)+-- Used for BigInt and BigSerial columns+instance DefaultParamEncoder Integer where+ defaultParam = Encoders.nonNullable (contramap fromInteger Encoders.int8)++-- | Encode 'Maybe Integer' as nullable PostgreSQL int8+instance DefaultParamEncoder (Maybe Integer) where+ defaultParam = Encoders.nullable (contramap fromInteger Encoders.int8)++-- | Encode 'Point' as PostgreSQL point via postgresql-types binary encoder+instance DefaultParamEncoder Point where+ defaultParam = Encoders.nonNullable Mapping.encoder++-- | Encode 'Maybe Point' as nullable PostgreSQL point+instance DefaultParamEncoder (Maybe Point) where+ defaultParam = Encoders.nullable Mapping.encoder++-- | Encode 'Polygon' as PostgreSQL polygon via postgresql-types binary encoder+instance DefaultParamEncoder Polygon where+ defaultParam = Encoders.nonNullable Mapping.encoder++-- | Encode 'Maybe Polygon' as nullable PostgreSQL polygon+instance DefaultParamEncoder (Maybe Polygon) where+ defaultParam = Encoders.nullable Mapping.encoder++-- | Encode 'Interval' as PostgreSQL interval via postgresql-types binary encoder+instance DefaultParamEncoder Interval where+ defaultParam = Encoders.nonNullable Mapping.encoder++-- | Encode 'Maybe Interval' as nullable PostgreSQL interval+instance DefaultParamEncoder (Maybe Interval) where+ defaultParam = Encoders.nullable Mapping.encoder++-- | Encode 'Tsvector' as PostgreSQL tsvector via postgresql-types binary encoder+instance DefaultParamEncoder Tsvector where+ defaultParam = Encoders.nonNullable Mapping.encoder++-- | Encode 'Maybe Tsvector' as nullable PostgreSQL tsvector+instance DefaultParamEncoder (Maybe Tsvector) where+ defaultParam = Encoders.nullable Mapping.encoder++-- | Encode 'Inet' as PostgreSQL inet via postgresql-types binary encoder+instance DefaultParamEncoder Inet where+ defaultParam = Encoders.nonNullable Mapping.encoder++-- | Encode 'Maybe Inet' as nullable PostgreSQL inet+instance DefaultParamEncoder (Maybe Inet) where+ defaultParam = Encoders.nullable Mapping.encoder++-- | Converts parameter tuples into a list of hasql 'Snippet' values.+--+-- This mirrors postgresql-simple's 'ToRow' typeclass, allowing @sqlQuery@ and @sqlExec@+-- to use hasql's native parameterized queries instead of 'PG.formatQuery'.+class ToSnippetParams a where+ toSnippetParams :: a -> [Snippet]++instance ToSnippetParams () where+ toSnippetParams () = []++instance DefaultParamEncoder a => ToSnippetParams (Only a) where+ toSnippetParams (Only a) = [Snippet.param a]++instance (DefaultParamEncoder a, DefaultParamEncoder b) => ToSnippetParams (a, b) where+ toSnippetParams (a, b) = [Snippet.param a, Snippet.param b]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c) => ToSnippetParams (a, b, c) where+ toSnippetParams (a, b, c) = [Snippet.param a, Snippet.param b, Snippet.param c]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c, DefaultParamEncoder d) => ToSnippetParams (a, b, c, d) where+ toSnippetParams (a, b, c, d) = [Snippet.param a, Snippet.param b, Snippet.param c, Snippet.param d]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c, DefaultParamEncoder d, DefaultParamEncoder e) => ToSnippetParams (a, b, c, d, e) where+ toSnippetParams (a, b, c, d, e) = [Snippet.param a, Snippet.param b, Snippet.param c, Snippet.param d, Snippet.param e]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c, DefaultParamEncoder d, DefaultParamEncoder e, DefaultParamEncoder f) => ToSnippetParams (a, b, c, d, e, f) where+ toSnippetParams (a, b, c, d, e, f) = [Snippet.param a, Snippet.param b, Snippet.param c, Snippet.param d, Snippet.param e, Snippet.param f]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c, DefaultParamEncoder d, DefaultParamEncoder e, DefaultParamEncoder f, DefaultParamEncoder g) => ToSnippetParams (a, b, c, d, e, f, g) where+ toSnippetParams (a, b, c, d, e, f, g) = [Snippet.param a, Snippet.param b, Snippet.param c, Snippet.param d, Snippet.param e, Snippet.param f, Snippet.param g]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c, DefaultParamEncoder d, DefaultParamEncoder e, DefaultParamEncoder f, DefaultParamEncoder g, DefaultParamEncoder h) => ToSnippetParams (a, b, c, d, e, f, g, h) where+ toSnippetParams (a, b, c, d, e, f, g, h) = [Snippet.param a, Snippet.param b, Snippet.param c, Snippet.param d, Snippet.param e, Snippet.param f, Snippet.param g, Snippet.param h]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c, DefaultParamEncoder d, DefaultParamEncoder e, DefaultParamEncoder f, DefaultParamEncoder g, DefaultParamEncoder h, DefaultParamEncoder i) => ToSnippetParams (a, b, c, d, e, f, g, h, i) where+ toSnippetParams (a, b, c, d, e, f, g, h, i) = [Snippet.param a, Snippet.param b, Snippet.param c, Snippet.param d, Snippet.param e, Snippet.param f, Snippet.param g, Snippet.param h, Snippet.param i]++instance (DefaultParamEncoder a, DefaultParamEncoder b, DefaultParamEncoder c, DefaultParamEncoder d, DefaultParamEncoder e, DefaultParamEncoder f, DefaultParamEncoder g, DefaultParamEncoder h, DefaultParamEncoder i, DefaultParamEncoder j) => ToSnippetParams (a, b, c, d, e, f, g, h, i, j) where+ toSnippetParams (a, b, c, d, e, f, g, h, i, j) = [Snippet.param a, Snippet.param b, Snippet.param c, Snippet.param d, Snippet.param e, Snippet.param f, Snippet.param g, Snippet.param h, Snippet.param i, Snippet.param j]++-- | Append two parameter lists (mirrors postgresql-simple's ':.' operator)+instance (ToSnippetParams a, ToSnippetParams b) => ToSnippetParams (a :. b) where+ toSnippetParams (a :. b) = toSnippetParams a <> toSnippetParams b++-- | Converts a SQL query with @?@ placeholders and a list of 'Snippet' parameters+-- into a single 'Snippet' with native hasql @$1, $2, ...@ parameterization.+--+-- This mirrors postgresql-simple's @?@ placeholder convention.+--+-- __Example:__+--+-- > sqlToSnippet "SELECT * FROM users WHERE id = ? AND name = ?" [Snippet.param id, Snippet.param name]+-- > -- becomes: Snippet.sql "SELECT * FROM users WHERE id = " <> Snippet.param id <> Snippet.sql " AND name = " <> Snippet.param name+--+sqlToSnippet :: ByteString -> [Snippet] -> Snippet+sqlToSnippet sql params = mconcat (interleave sqlParts params)+ where+ sqlParts = map (Snippet.sql . Text.decodeUtf8) (BS8.split '?' sql)+ interleave (s:ss) (p:ps) = s : p : interleave ss ps+ interleave ss [] = ss+ interleave [] _ = []+{-# INLINE sqlToSnippet #-}
@@ -0,0 +1,88 @@+{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}+{-# OPTIONS_GHC -Wno-orphans #-}+{-|+Module: IHP.Hasql.FromRow+Description: Typeclass for decoding hasql result rows+Copyright: (c) digitally induced GmbH, 2025++This module provides 'FromRowHasql', a typeclass parallel to postgresql-simple's 'FromRow',+for decoding database rows using hasql's more efficient prepared statement approach.++Instances are generated by the SchemaCompiler with explicit inline decoders in idiomatic+hasql applicative style.++Also provides parser functions used by the generated decoders for custom PostgreSQL types.+-}+module IHP.Hasql.FromRow+( FromRowHasql (..)+, HasqlDecodeColumn (..)+) where++import Prelude+import qualified Hasql.Decoders as Decoders+import qualified Hasql.Mapping.IsScalar as Mapping+import qualified Database.PostgreSQL.Simple.Types as PG+import Data.Functor.Contravariant (contramap)+import IHP.ModelSupport.Types (LabeledData(..), Id'(..), PrimaryKey)++-- | Typeclass for types that can be decoded from a hasql result row+--+-- This is the hasql equivalent of postgresql-simple's 'FromRow' class.+-- The SchemaCompiler generates instances for all model types using idiomatic+-- hasql applicative style with explicit inline decoders.+class FromRowHasql a where+ -- | Decoder for a single row+ hasqlRowDecoder :: Decoders.Row a++-- | Typeclass for building column-level row decoders, handling nullable/non-nullable.+-- Uses 'Mapping.IsScalar' from hasql-mapping for value-level decoding.+class HasqlDecodeColumn a where+ hasqlColumnDecoder :: Decoders.Row a++instance {-# OVERLAPPABLE #-} Mapping.IsScalar a => HasqlDecodeColumn a where+ hasqlColumnDecoder = Decoders.column (Decoders.nonNullable Mapping.decoder)++instance {-# OVERLAPPING #-} Mapping.IsScalar a => HasqlDecodeColumn (Maybe a) where+ hasqlColumnDecoder = Decoders.column (Decoders.nullable Mapping.decoder)++-- | IHP's schema maps SQL @INT@ (int4) to Haskell 'Int', but hasql-mapping's+-- @IsScalar Int@ instance uses @int8@ (bigint). Override to match IHP conventions.+instance {-# OVERLAPPING #-} HasqlDecodeColumn Int where+ hasqlColumnDecoder = Decoders.column (Decoders.nonNullable (fromIntegral <$> Decoders.int4))++instance {-# OVERLAPPING #-} HasqlDecodeColumn (Maybe Int) where+ hasqlColumnDecoder = Decoders.column (Decoders.nullable (fromIntegral <$> Decoders.int4))++-- | 'IsScalar' instance for 'Id'' so that Id columns can use 'Mapping.encoder' and 'Mapping.decoder'+-- directly in generated code, without manual wrapping/unwrapping.+instance Mapping.IsScalar (PrimaryKey table) => Mapping.IsScalar (Id' table) where+ encoder = contramap (\(Id pk) -> pk) Mapping.encoder+ decoder = Id <$> Mapping.decoder++-- | Decode 'Id' table' by decoding the primary key type and wrapping with 'Id'+instance {-# OVERLAPPING #-} Mapping.IsScalar (PrimaryKey table) => HasqlDecodeColumn (Id' table) where+ hasqlColumnDecoder = Decoders.column (Decoders.nonNullable Mapping.decoder)++-- | Decode 'Maybe (Id' table)' for nullable foreign keys+instance {-# OVERLAPPING #-} Mapping.IsScalar (PrimaryKey table) => HasqlDecodeColumn (Maybe (Id' table)) where+ hasqlColumnDecoder = Decoders.column (Decoders.nullable Mapping.decoder)++-- FromRowHasql instances for PG.Only and tuples (used by sqlQuery callers like fetchCount, fetchExists)++instance HasqlDecodeColumn a => FromRowHasql (PG.Only a) where+ hasqlRowDecoder = PG.Only <$> hasqlColumnDecoder++instance (HasqlDecodeColumn a, HasqlDecodeColumn b) => FromRowHasql (a, b) where+ hasqlRowDecoder = (,) <$> hasqlColumnDecoder <*> hasqlColumnDecoder++instance (HasqlDecodeColumn a, HasqlDecodeColumn b, HasqlDecodeColumn c) => FromRowHasql (a, b, c) where+ hasqlRowDecoder = (,,) <$> hasqlColumnDecoder <*> hasqlColumnDecoder <*> hasqlColumnDecoder++instance (HasqlDecodeColumn a, HasqlDecodeColumn b, HasqlDecodeColumn c, HasqlDecodeColumn d) => FromRowHasql (a, b, c, d) where+ hasqlRowDecoder = (,,,) <$> hasqlColumnDecoder <*> hasqlColumnDecoder <*> hasqlColumnDecoder <*> hasqlColumnDecoder++instance (HasqlDecodeColumn a, HasqlDecodeColumn b, HasqlDecodeColumn c, HasqlDecodeColumn d, HasqlDecodeColumn e) => FromRowHasql (a, b, c, d, e) where+ hasqlRowDecoder = (,,,,) <$> hasqlColumnDecoder <*> hasqlColumnDecoder <*> hasqlColumnDecoder <*> hasqlColumnDecoder <*> hasqlColumnDecoder++instance (HasqlDecodeColumn label, FromRowHasql a) => FromRowHasql (LabeledData label a) where+ hasqlRowDecoder = LabeledData <$> hasqlColumnDecoder <*> hasqlRowDecoder
@@ -0,0 +1,48 @@+module IHP.Hasql.Pool (usePoolWithRetry) where++import Prelude+import Control.Exception (throwIO)+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Session as Hasql+import qualified Hasql.Errors as HasqlErrors+import IHP.ModelSupport.Types (HasqlError(..))++-- | Run a session on the pool, retrying on stale prepared-statement errors.+--+-- After schema changes (e.g. @make db@), pooled connections have stale caches.+-- hasql-pool auto-discards these connections, so retrying cycles through the+-- pool until a fresh connection is created. Retries are bounded to avoid+-- infinite loops if the error is persistent rather than transient.+usePoolWithRetry :: HasqlPool.Pool -> Hasql.Session a -> IO a+usePoolWithRetry pool session = go maxRetries+ where+ -- Generous upper bound. In practice the pool has far fewer connections,+ -- so a fresh connection is reached well before this limit.+ maxRetries :: Int+ maxRetries = 32++ go 0 = do+ result <- HasqlPool.use pool session+ case result of+ Left err -> throwIO (HasqlError err)+ Right a -> pure a+ go !n = do+ result <- HasqlPool.use pool session+ case result of+ Left err+ | isCachedPlanError err -> go (n - 1)+ | otherwise -> throwIO (HasqlError err)+ Right a -> pure a++isCachedPlanError :: HasqlPool.UsageError -> Bool+isCachedPlanError (HasqlPool.SessionUsageError sessionError) = isCachedPlanSessionError sessionError+isCachedPlanError _ = False++isCachedPlanSessionError :: HasqlErrors.SessionError -> Bool+isCachedPlanSessionError (HasqlErrors.StatementSessionError _ _ _ _ _ (HasqlErrors.ServerStatementError (HasqlErrors.ServerError "0A000" _ _ _ _))) = True+isCachedPlanSessionError (HasqlErrors.StatementSessionError _ _ _ _ _ (HasqlErrors.ServerStatementError (HasqlErrors.ServerError "XX000" _ _ _ _))) = True+isCachedPlanSessionError (HasqlErrors.ScriptSessionError _ (HasqlErrors.ServerError "0A000" _ _ _ _)) = True+isCachedPlanSessionError (HasqlErrors.ScriptSessionError _ (HasqlErrors.ServerError "XX000" _ _ _ _)) = True+isCachedPlanSessionError (HasqlErrors.MissingTypesSessionError _) = True+isCachedPlanSessionError (HasqlErrors.StatementSessionError _ _ _ _ _ (HasqlErrors.UnexpectedColumnTypeStatementError _ _ _)) = True+isCachedPlanSessionError _ = False
@@ -0,0 +1,92 @@+module IHP.InputValue+( InputValue (..)+) where++import Prelude+import Data.Text (Text)+import qualified Data.Text as Text+import Data.UUID (UUID)+import qualified Data.UUID as UUID+import Data.Time.Clock (UTCTime)+import Data.Time.LocalTime (LocalTime, TimeOfDay)+import Data.Time.Calendar (Day)+import Data.Time.Format.ISO8601 (iso8601Show)+import Data.Aeson (Value)+import qualified Data.Aeson as Aeson+import Data.Scientific (Scientific)+import Data.String.Conversions (cs)+import qualified Numeric+import PostgresqlTypes.Interval (Interval)+import PostgresqlTypes.Inet (Inet)++-- | Provides a way to convert a Haskell value to a Text representation+-- for use in HTML @\<input value="..."\/>@ attributes.+--+-- This is used by IHP's form helpers to populate form field values.+--+-- __Example:__+--+-- >>> inputValue True+-- "on"+--+-- >>> inputValue (1 :: Int)+-- "1"+--+class InputValue a where+ inputValue :: a -> Text++instance InputValue Text where+ inputValue text = text++instance InputValue Int where+ inputValue = Text.pack . show++instance InputValue Integer where+ inputValue = Text.pack . show++instance InputValue Double where+ inputValue double = cs (Numeric.showFFloat Nothing double "")++instance InputValue Float where+ inputValue float = cs (Numeric.showFFloat Nothing float "")++instance InputValue Bool where+ inputValue True = "on"+ inputValue False = "off"++instance InputValue UUID where+ inputValue = UUID.toText++instance InputValue () where+ inputValue () = "error: inputValue(()) not supported"++instance InputValue UTCTime where+ inputValue time = cs (iso8601Show time)++instance InputValue LocalTime where+ inputValue time = cs (iso8601Show time)++instance InputValue Day where+ inputValue date = cs (iso8601Show date)++instance InputValue TimeOfDay where+ inputValue timeOfDay = Text.pack (show timeOfDay)++instance InputValue Interval where+ inputValue interval = Text.pack (show interval)++instance InputValue Inet where+ inputValue inet = Text.pack (show inet)++instance InputValue fieldType => InputValue (Maybe fieldType) where+ inputValue (Just value) = inputValue value+ inputValue Nothing = ""++instance InputValue value => InputValue [value] where+ inputValue list = Text.intercalate "," (map inputValue list)++instance InputValue Value where+ inputValue json = cs (Aeson.encode json)++instance InputValue Scientific where+ inputValue = Text.pack . show
@@ -1,400 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}--{-|-Module: IHP.Job.Dashboard-Description: Auto-generate a dashboard for job types--This module allows IHP applications to generate a dashboard for interacting with job types.-To start, first define a type for the dashboard:--> type MyDashboard = JobsDashboardController NoAuth '[]--And include the following in the 'controllers' list of a FrontController:--> parseRoute @MyDashboard--This generates a dashboard with listings for all tables which have names ending with "_jobs".--All views are fully customizable. For more info, see the documentation for 'DisplayableJob'.-If you implement custom behavior for a job type, add it to the list in the Dashboard type:--> type MyDashboard = JobsDashboardController NoAuth '[EmailUserJob, UpdateRecordJob]--}-module IHP.Job.Dashboard (- module IHP.Job.Dashboard.View,- module IHP.Job.Dashboard.Auth,- module IHP.Job.Dashboard.Types,-- JobsDashboard(..),- DisplayableJob(..),- JobsDashboardController(..),- getTableName,-) where--import IHP.Prelude-import IHP.ModelSupport-import IHP.ControllerPrelude-import Unsafe.Coerce-import IHP.Job.Queue ()-import IHP.Pagination.Types-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Database.PostgreSQL.Simple.FromField as PG-import qualified Database.PostgreSQL.Simple.ToField as PG-import Network.Wai (requestMethod)-import Network.HTTP.Types.Method (methodPost)--import IHP.Job.Dashboard.Types-import IHP.Job.Dashboard.View-import IHP.Job.Dashboard.Auth-import IHP.Job.Dashboard.Utils---- | The crazy list of type constraints for this class defines everything needed for a generic "Job".--- All jobs created through the IHP dev IDE will automatically satisfy these constraints and thus be able to--- be used as a 'DisplayableJob'.--- To customize the dashboard behavior for each job, you should provide a custom implementation of 'DisplayableJob'--- for your job type. Your custom implementations will then be used instead of the defaults.-class ( job ~ GetModelByTableName (GetTableName job)- , FilterPrimaryKey (GetTableName job)- , FromRow job- , Show (PrimaryKey (GetTableName job))- , PG.FromField (PrimaryKey (GetTableName job))- , PG.ToField (PrimaryKey (GetTableName job))- , KnownSymbol (GetTableName job)- , HasField "id" job (Id job)- , HasField "status" job JobStatus- , HasField "updatedAt" job UTCTime- , HasField "createdAt" job UTCTime- , HasField "lastError" job (Maybe Text)- , CanUpdate job- , CanCreate job- , Record job- , Show job- , Eq job- , Table job- , Typeable job) => DisplayableJob job where-- -- | How this job's section should be displayed in the dashboard. By default it's displayed as a table,- -- but this can be any arbitrary view! Make some cool graphs :)- makeDashboardSection :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO SomeView-- makePageView :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Int -> Int -> IO SomeView-- -- | The content of the page that will be displayed for a detail view of this job.- -- By default, the ID, Status, Created/Updated at times, and last error are displayed.- -- Can be defined as any arbitrary view.- makeDetailView :: (?context :: ControllerContext, ?modelContext :: ModelContext) => job -> IO SomeView- makeDetailView job = do- pure $ SomeView $ HtmlView $ renderBaseJobDetailView (buildBaseJob job)-- -- | The content of the page that will be displayed for the "new job" form of this job.- -- By default, only the submit button is rendered. For additonal form data, define your own implementation.- -- Can be defined as any arbitrary view, but it should be a form.- makeNewJobView :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO SomeView- makeNewJobView = pure $ SomeView $ HtmlView $ renderNewBaseJobForm $ tableName @job-- -- | The action run to create and insert a new value of this job into the database.- -- By default, create an empty record and insert it.- -- To add more data, define your own implementation.- createNewJob :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO ()- createNewJob = do- newRecord @job |> create- pure ()------- | Defines implementations for actions for acting on a dashboard made of some list of types.--- This is included to allow these actions to recurse on the types, isn't possible in an IHP Controller--- action implementation.------ Later functions and typeclasses introduce constraints on the types in this list,--- so you'll get a compile error if you try and include a type that is not a job.-class JobsDashboard (jobs :: [Type]) where- -- | Creates the entire dashboard by recursing on the type list and calling 'makeDashboardSection' on each type.- makeDashboard :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO SomeView-- includedJobTables :: [Text]-- -- | Renders the index page, which is the view returned from 'makeDashboard'.- indexPage :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO ()-- listJob :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Text -> IO ()- listJob' :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Bool -> IO ()-- -- | Renders the detail view page. Rescurses on the type list to find a type with the- -- same table name as the "tableName" query parameter.- viewJob :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Text -> UUID -> IO ()- viewJob' :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Bool -> IO ()-- -- | If performed in a POST request, creates a new job depending on the "tableName" query parameter.- -- If performed in a GET request, renders the new job from depending on said parameter.- newJob :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Text -> IO ()- newJob' :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Bool -> IO ()-- -- | Deletes a job from the database.- deleteJob :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Text -> UUID -> IO ()- deleteJob' :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Bool -> IO ()-- retryJob :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Text -> UUID -> IO ()- retryJob' :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO ()---- If no types are passed, try to get all tables dynamically and render them as BaseJobs-instance JobsDashboard '[] where-- -- | Invoked at the end of recursion- makeDashboard = pure $ SomeView $ HtmlView [hsx|- <script>- function initPopover() {- $('[data-toggle="popover"]').popover({ trigger: 'hover click' })- }- $(document).on('ready turbolinks:load', initPopover);- $(initPopover);- </script>- <style>- .popover-body {- background-color: #01313f;- color: rgb(147, 161, 161);- font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;- font-size: 11px;- }- </style>- |]-- includedJobTables = []-- indexPage = do- tableNames <- getAllTableNames- tables <- mapM buildBaseJobTable tableNames- render $ SomeView tables- where- getAllTableNames = map extractText <$> sqlQuery- "SELECT table_name FROM information_schema.tables WHERE table_name LIKE '%_jobs'" ()-- listJob = error "listJob: Requested job type not in JobsDashboard Type"- listJob' _ = do- let table = param "tableName"- options = defaultPaginationOptions- page = paramOrDefault 1 "page"- pageSize = paramOrDefault (maxItems options) "maxItems"- totalItems <- totalRecordsForTable table- jobs <- queryBaseJobsFromTablePaginated table (page - 1) pageSize- let pagination = Pagination { currentPage = page, totalItems, pageSize, window = windowSize options }- render $ HtmlView $ renderBaseJobTablePaginated table jobs pagination-- viewJob = error "viewJob: Requested job type not in JobsDashboard Type"- viewJob' _ = do- baseJob <- queryBaseJob (param "tableName") (param "id")- render $ HtmlView $ renderBaseJobDetailView baseJob-- newJob = error "newJob: Requested job type not in JobsDashboard Type"- newJob' _ = do- if requestMethod request == methodPost- then do- insertJob- setSuccessMessage (columnNameToFieldLabel (param "tableName") <> " job started.")- redirectTo ListJobsAction- else render $ HtmlView $ renderNewBaseJobForm (param "tableName")- where insertJob = sqlExec (PG.Query $ "INSERT into " <> param "tableName" <> " DEFAULT VALUES") ()-- deleteJob = error "deleteJob: Requested job type not in JobsDashboard Type"- deleteJob' _ = do- let id :: UUID = param "id"- table :: Text = param "tableName"- delete id table- setSuccessMessage (columnNameToFieldLabel table <> " record deleted.")- redirectTo ListJobsAction-- where delete id table = sqlExec (PG.Query $ cs $ "DELETE FROM " <> table <> " WHERE id = ?") (Only id)-- retryJob = error "retryJob: Requested job type not in JobsDashboard Type"- retryJob' = do- let id :: UUID = param "id"- table :: Text = param "tableName"- retryJobById table id- setSuccessMessage (columnNameToFieldLabel table <> " record marked as 'retry'.")- redirectTo ListJobsAction-- where retryJobById table id = sqlExec ("UPDATE ? SET status = 'job_status_retry' WHERE id = ?") (PG.Identifier table, id)----- | Defines the default implementation for a dashboard of a list of job types.--- We know the current job is a 'DisplayableJob', and we can recurse on the rest of the list to build the rest of the dashboard.--- You probably don't want to provide custom implementations for these. Read the documentation for each of the functions if--- you'd like to know how to customize the behavior. They mostly rely on the functions from 'DisplayableJob'.-instance {-# OVERLAPPABLE #-} (DisplayableJob job, JobsDashboard rest) => JobsDashboard (job:rest) where-- -- | Recusively create a list of views that are concatenated together as 'SomeView's to build the dashboard.- -- To customize, override 'makeDashboardSection' for each job.- makeDashboard = do- section <- makeDashboardSection @job- restSections <- SomeView <$> makeDashboard @rest- pure $ SomeView (section : [restSections])-- -- | Recursively build list of included table names- includedJobTables = tableName @job : includedJobTables @rest-- -- | Build the dashboard and render it.- indexPage = do- dashboardIncluded <- makeDashboard @(job:rest)- notIncluded <- getNotIncludedTableNames (includedJobTables @(job:rest))- baseJobTables <- mapM buildBaseJobTable notIncluded- render $ dashboardIncluded : baseJobTables-- listJob table = do- let page = fromMaybe 1 $ param "page"- page <- makePageView @job page 25- render page-- listJob' isFirstTime = do- let table = param "tableName"-- when isFirstTime $ do- notIncluded <- getNotIncludedTableNames (includedJobTables @(job:rest))- when (table `elem` notIncluded) (listJob' @'[] False)-- if tableName @job == table- then listJob @(job:rest) table- else listJob' @rest False-- -- | View the detail page for the job with a given uuid.- viewJob _ uuid = do- let id :: Id job = unsafeCoerce uuid- j <- fetch id- view <- makeDetailView @job j- render view-- -- | For a given "tableName" parameter, try and recurse over the list of types- -- in order to find a type with the some table name as the parameter.- -- If one is found, attempt to construct an ID from the "id" parameter,- -- and render a page using the type's implementation of 'makeDetailView'.- -- If you want to customize the page, override that function instead.- viewJob' isFirstTime = do- let table = param "tableName"-- when isFirstTime $ do- notIncluded <- getNotIncludedTableNames (includedJobTables @(job:rest))- when (table `elem` notIncluded) (viewJob' @'[] False)-- if tableName @job == table- then viewJob @(job:rest) table (param "id")- else viewJob' @rest False-- -- For POST, create a new job using the job's implementation of 'createNewJob'.- -- To include other request data and parameters, override that function, not this one.- -- If it's a GET request, render a new job form with the job's implementation of 'makeNewJobView'.- -- For customizing this form, override 'makeNewJobView'.- newJob tableName = do- if requestMethod request == methodPost- then do- createNewJob @job- setSuccessMessage (columnNameToFieldLabel tableName <> " job started.")- redirectTo ListJobsAction- else do- view <- makeNewJobView @job- render view-- -- | For a given "tableName" parameter, try and recurse over the list of types- -- in order to find a type with the some table name as the parameter.- -- If such a type is found, call newJob.- newJob' isFirstTime = do- let table = param "tableName"-- when isFirstTime $ do- notIncluded <- getNotIncludedTableNames (includedJobTables @(job:rest))- when (table `elem` notIncluded) (newJob' @'[] False)-- if tableName @job == table- then newJob @(job:rest) table- else newJob' @rest False-- -- | Delete job in 'table' with ID 'uuid'.- deleteJob table uuid = do- let id :: Id job = unsafeCoerce uuid- deleteRecordById @job id- setSuccessMessage (columnNameToFieldLabel table <> " record deleted.")- redirectTo ListJobsAction-- -- | For a given "tableName" parameter, try and recurse over the list of types- -- in order to find a type with the some table name as the parameter.- -- If one is found, delete the record with the given id.- deleteJob' isFirstTime = do- let table = param "tableName"-- when isFirstTime $ do- notIncluded <- getNotIncludedTableNames (includedJobTables @(job:rest))- when (table `elem` notIncluded) (deleteJob' @'[] False)-- if tableName @job == table- then deleteJob @(job:rest) table (param "id")- else deleteJob' @rest False-- retryJob table uuid = do- let id :: UUID = param "id"- table :: Text = param "tableName"- retryJobById table id = sqlExec ("UPDATE ? SET status = 'job_status_retry' WHERE id = ?") (PG.Identifier table, id)- retryJobById table id- setSuccessMessage (columnNameToFieldLabel table <> " record marked as 'retry'.")- redirectTo ListJobsAction- retryJob' = do- let table = param "tableName"-- if tableName @job == table- then retryJob @(job:rest) table (param "id")- else retryJob' @rest--extractText = \(Only t) -> t-getNotIncludedTableNames includedNames = map extractText <$> sqlQuery- "SELECT table_name FROM information_schema.tables WHERE table_name LIKE '%_jobs' AND table_name NOT IN ?"- (Only $ In $ includedNames)-buildBaseJobTable :: (?modelContext :: ModelContext, ?context :: ControllerContext) => Text -> IO SomeView-buildBaseJobTable tableName = do- baseJobs <- sqlQuery (PG.Query $ cs $ queryString) (Only tableName)- baseJobs- |> renderBaseJobTable tableName- |> HtmlView- |> SomeView- |> pure-- where- queryString = "SELECT ?, id, status, updated_at, created_at, last_error FROM "- <> tableName- <> " ORDER BY created_at DESC LIMIT 10"--buildBaseJob :: forall job. (DisplayableJob job) => job -> BaseJob-buildBaseJob job = BaseJob- (tableName @job)- (unsafeCoerce $ job.id) -- model Id type -> UUID. Pls don't use integer IDs for your jobs :)- (job.status)- (job.updatedAt)- (job.createdAt)- (job.lastError)----- | We can't always access the type of our job in order to use type application syntax for 'tableName'.--- This is just a convinence function for those cases.-getTableName :: forall job. (DisplayableJob job) => job -> Text-getTableName _ = tableName @job---- | Get the job with in the given table with the given ID as a 'BaseJob'.-queryBaseJob :: (?modelContext :: ModelContext) => Text -> UUID -> IO BaseJob-queryBaseJob table id = do- (job : _) <- sqlQuery- (PG.Query $ cs $ "select ?, id, status, updated_at, created_at, last_error from " <> table <> " where id = ?")- [table, tshow id]- pure job--queryBaseJobsFromTablePaginated :: (?modelContext :: ModelContext) => Text -> Int -> Int -> IO [BaseJob]-queryBaseJobsFromTablePaginated table page pageSize =- sqlQuery- (PG.Query $ cs $ "select ?, id, status, updated_at, created_at, last_error from " <> table <> " OFFSET " <> tshow (page * pageSize) <> " LIMIT " <> tshow pageSize)- (Only table)--instance (JobsDashboard jobs, AuthenticationMethod authType) => Controller (JobsDashboardController authType jobs) where- beforeAction = authenticate @authType- action ListJobsAction = autoRefresh $ indexPage @jobs- action ListJobAction' = autoRefresh $ listJob' @jobs True- action ViewJobAction' = autoRefresh $ viewJob' @jobs True- action CreateJobAction' = newJob' @jobs True- action DeleteJobAction' = deleteJob' @jobs True- action RetryJobAction' = retryJob' @jobs- action _ = error "Cannot call this action directly. Call the backtick function with no parameters instead."
@@ -1,52 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}--{-|-Module: IHP.Job.Dashboard.Auth-Description: Authentication for Job dashboard--}-module IHP.Job.Dashboard.Auth (- AuthenticationMethod(..),- NoAuth(..),- BasicAuth(..),- BasicAuthStatic(..),-) where--import IHP.Prelude-import IHP.ControllerPrelude-import qualified IHP.EnvVar as EnvVar---- | Defines one method, 'authenticate', called before every action. Use to authenticate user.------ Three implementations are provided:--- - 'NoAuth' : No authentication--- - 'BasicAuth' : HTTP Basic Auth using environment variables--- - 'BasicAuthStatic' : HTTP Basic Auth using static values------ Define your own implementation to use custom authentication for production.-class AuthenticationMethod a where- authenticate :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO ()---- | Don't use any authentication for jobs.-data NoAuth---- | Authenticate using HTTP Basic Authentication by looking up username/password values--- in environment variables given as type-level strings.-data BasicAuth (userEnv :: Symbol) (passEnv :: Symbol)---- | Authenticate using HTTP Basic Authentication using username/password given as type level strings.--- Meant for development only!-data BasicAuthStatic (user :: Symbol) (pass :: Symbol)--instance AuthenticationMethod NoAuth where- authenticate = pure ()--instance (KnownSymbol userEnv, KnownSymbol passEnv) => AuthenticationMethod (BasicAuth userEnv passEnv) where- authenticate = do- creds <- (,) <$> EnvVar.envOrNothing (symbolToByteString @userEnv) <*> EnvVar.envOrNothing (symbolToByteString @passEnv)- case creds of- (Just user, Just pass) -> basicAuth user pass "jobs"- _ -> error "Did not find HTTP Basic Auth credentials for Jobs Dashboard."--instance (KnownSymbol user, KnownSymbol pass) => AuthenticationMethod (BasicAuthStatic user pass) where- authenticate = basicAuth (cs $ symbolVal $ Proxy @user) (cs $ symbolVal $ Proxy @pass) "jobs"-
@@ -1,112 +0,0 @@-{-|-Module: IHP.Job.Dashboard.Types-Description: Types for Job dashboard--}-{-# LANGUAGE AllowAmbiguousTypes #-}-module IHP.Job.Dashboard.Types (- BaseJob(..),- JobsDashboardController(..),- TableViewable(..),- IncludeWrapper(..),-) where--import IHP.Prelude-import IHP.ControllerPrelude-import IHP.ViewPrelude (Html)-import IHP.RouterPrelude hiding (get, tshow, error, map, putStrLn, elem)-import Database.PostgreSQL.Simple.FromRow (FromRow(..), field)-import IHP.Job.Queue () -- get FromField definition for JobStatus---data BaseJob = BaseJob {- table :: Text- , id :: UUID- , status :: JobStatus- , updatedAt :: UTCTime- , createdAt :: UTCTime- , lastError :: Maybe Text-} deriving (Show)--class TableViewable a where- -- | Human readable title displayed on the table- tableTitle :: Text-- -- | Database table backing the view- modelTableName :: Text-- tableHeaders :: [Text]- renderTableRow :: a -> Html-- -- | Link used in the table to send user to new job form- newJobLink :: Html-- -- | Gets records for displaying in the dashboard index page- getIndex :: (?context :: ControllerContext, ?modelContext :: ModelContext) => IO [a]-- -- | Gets paginated records for displaying in the list page- getPage :: (?context :: ControllerContext, ?modelContext :: ModelContext) => Int -> Int -> IO [a]--instance FromRow BaseJob where- fromRow = BaseJob <$> field <*> field <*> field <*> field <*> field <*> field---- | Often, jobs are related to some model type. These relations are modeled through the type system.--- For example, the type 'Include "userId" UpdateUserJob' models an 'UpdateUserJob' type that can access--- the 'User' it belongs to through the 'userId' field.--- For some reason, GHC doesn't allow us to create implementations of type family applications, so the following doesn't work:------ > instance DisplayableJob (Include "userId" UpdateUserJob) where------ However, if we wrap this in a concrete type, it works fine. That's what this wrapper is for.--- To get the same behavior as above, just define------ > instance DisplayableJob (IncludeWrapper "userId" UpdateUserJob) where------ and wrap the values as so:------ > jobsWithUsers <- query @UpdateUserJob--- > |> fetch--- > >>= mapM (fetchRelated #userId)--- > >>= pure . map (IncludeWrapper @"userId" @UpdateUserJob)-newtype IncludeWrapper (id :: Symbol) job = IncludeWrapper (Include id job)---- | Defines controller actions for acting on a dashboard made of some list of types.--- Later functions and typeclasses introduce constraints on the types in this list,--- so you'll get a compile error if you try and include a type that is not a job.-data JobsDashboardController authType (jobs :: [Type])- = ListJobsAction- | ListJobAction { jobTableName :: Text, page :: Int }- -- These actions are used for 'pathTo'. Need to pass the parameters explicity to know how to build the path- | ViewJobAction { jobTableName :: Text, jobId :: UUID }- | CreateJobAction { jobTableName :: Text }- | DeleteJobAction { jobTableName :: Text, jobId :: UUID }- | RetryJobAction { jobTableName :: Text, jobId :: UUID }-- -- These actions are used for interal routing. Parameters are extracted dynamically in the action based on types- | ListJobAction'- | ViewJobAction'- | CreateJobAction'- | DeleteJobAction'- | RetryJobAction'- deriving (Show, Eq, Data)---instance HasPath (JobsDashboardController authType jobs) where- pathTo ListJobsAction = "/jobs/ListJobs"- pathTo ListJobAction { .. } = "/jobs/ListJob?tableName=" <> jobTableName <> "&page=" <> tshow page- pathTo ViewJobAction { .. } = "/jobs/ViewJob?tableName=" <> jobTableName <> "&id=" <> tshow jobId- pathTo CreateJobAction { .. } = "/jobs/CreateJob?tableName=" <> jobTableName- pathTo DeleteJobAction { .. } = "/jobs/DeleteJob?tableName=" <> jobTableName <> "&id=" <> tshow jobId- pathTo RetryJobAction { .. } = "/jobs/RetryJob?tableName=" <> jobTableName <> "&id=" <> tshow jobId- pathTo _ = error "pathTo for internal JobsDashboard functions not supported. Use non-backtick action and pass necessary parameters to use pathTo."--instance CanRoute (JobsDashboardController authType jobs) where- parseRoute' = do- (string "/jobs" <* endOfInput >> pure ListJobsAction)- <|> (string "/jobs/" <* endOfInput >> pure ListJobsAction)- <|> (string "/jobs/ListJobs" <* endOfInput >> pure ListJobsAction)- <|> (string "/jobs/ListJob" <* endOfInput >> pure ListJobAction')- <|> (string "/jobs/ViewJob" <* endOfInput >> pure ViewJobAction')- <|> (string "/jobs/CreateJob" <* endOfInput >> pure CreateJobAction')- <|> (string "/jobs/DeleteJob" <* endOfInput >> pure DeleteJobAction')- <|> (string "/jobs/RetryJob" <* endOfInput >> pure RetryJobAction')-
@@ -1,15 +0,0 @@-module IHP.Job.Dashboard.Utils where--import IHP.Prelude-import IHP.ModelSupport-import qualified Database.PostgreSQL.Simple.Types as PG--numberOfPagesForTable :: (?modelContext::ModelContext) => Text -> Int -> IO Int-numberOfPagesForTable table pageSize = do- totalRecords <- totalRecordsForTable table- pure $ case totalRecords `quotRem` pageSize of- (pages, 0) -> pages- (pages, _) -> pages + 1--totalRecordsForTable :: (?modelContext :: ModelContext) => Text -> IO Int-totalRecordsForTable table = sqlQueryScalar (PG.Query $ cs $ "SELECT COUNT(*) FROM " <> table) ()
@@ -1,305 +0,0 @@-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE GADTs #-}--{-|-Module: IHP.Job.Dashboard.View-Description: Views for Job dashboard--}-module IHP.Job.Dashboard.View where--import IHP.Prelude-import IHP.ViewPrelude (JobStatus(..), ControllerContext, Html, View, hsx, html, timeAgo)-import qualified Data.List as List-import IHP.Job.Dashboard.Types-import IHP.Job.Dashboard.Utils-import IHP.Pagination.Types-import IHP.Pagination.ViewFunctions---- | Provides a type-erased view. This allows us to specify a view as a return type without needed--- to know exactly what type the view will be, which in turn allows for custom implmentations of--- almost all the view functions in this module. Go GADTs!-data SomeView where- SomeView :: forall a. (View a) => a -> SomeView---- | Since the only constructor for 'SomeView' requires that it is passed a 'View', we can use--- that to implement a 'View' instance for 'SomeView'-instance View SomeView where- html (SomeView a) = let ?view = a in IHP.ViewPrelude.html a---- | Define how to render a list of views as a view. Just concatenate them together!-instance (View a) => View [a] where- html [] = [hsx||]- html (x:xs) =- -- need to nest let's here in order to satisfy the implicit ?view parameter for 'html'.- -- ?view needs to be the type of the view being rendered, so set it before each render- -- here we render single view- let ?view = x in- let current = IHP.ViewPrelude.html x in- -- now rendering a list view- let ?view = xs in- let rest = IHP.ViewPrelude.html xs in- [hsx|{current}{rest}|]---- | A view containing no data. Used occasionally as a default implementation for some functions.-data EmptyView = EmptyView-instance View EmptyView where- html _ = [hsx||]---- | A view constructed from some HTML.-newtype HtmlView = HtmlView Html-instance View HtmlView where- html (HtmlView html) = [hsx|{html}|]--renderStatus job = case job.status of- JobStatusNotStarted -> [hsx|<span class="badge badge-secondary">Not Started</span>|]- JobStatusRunning -> [hsx|<span class="badge badge-primary">Running</span>|]- JobStatusFailed -> [hsx|<span class="badge badge-danger" title="Last Error" data-container="body" data-toggle="popover" data-placement="left" data-content={fromMaybe "" (job.lastError)}>Failed</span>|]- JobStatusSucceeded -> [hsx|<span class="badge badge-success">Succeeded</span>|]- JobStatusRetry -> [hsx|<span class="badge badge-warning" title="Last Error" data-container="body" data-toggle="popover" data-placement="left" data-content={fromMaybe "" (job.lastError)}>Retry</span>|]- JobStatusTimedOut -> [hsx|<span class="badge badge-danger" >Timed Out</span>|]---- BASE JOB VIEW HELPERS ----------------------------------renderBaseJobTable :: Text -> [BaseJob] -> Html-renderBaseJobTable table rows =- let- headers :: [Text] = ["ID", "Updated At", "Status", "", ""]- humanTitle = table |> columnNameToFieldLabel- in [hsx|- <div>- <div class="d-flex justify-content-between align-items-center">- <h3>{humanTitle}</h3>- {renderNewBaseJobLink table}- </div>- <table class="table table-sm table-hover">- <thead>- <tr>- {forEach headers renderHeader}- </tr>- </thead>-- <tbody>- {forEach rows renderBaseJobTableRow}- </tbody>- </table>- <a href={ListJobAction table 1} class="link-primary">See all {humanTitle}</a>- <hr />- </div>-|]- where renderHeader field = [hsx|<th>{field}</th>|]--renderBaseJobTablePaginated :: Text -> [BaseJob] -> Pagination -> Html-renderBaseJobTablePaginated table jobs pagination =- let- headers :: [Text] = ["ID", "Updated At", "Status", "", ""]- lastJobIndex = (List.length jobs) - 1- in- [hsx|- <div>- <div class="d-flex justify-content-between align-items-center">- <h3>{table |> columnNameToFieldLabel}</h3>- {renderNewBaseJobLink table}- </div>- <table class="table table-sm table-hover">- <thead>- <tr>- {forEach headers renderHeader}- </tr>- </thead>-- <tbody>- {forEach jobs renderBaseJobTableRow}- </tbody>- </table>- </div>- {renderPagination pagination}- |]- where- renderHeader field = [hsx|<th>{field}</th>|]--renderBaseJobTableRow :: BaseJob -> Html-renderBaseJobTableRow job = [hsx|- <tr>- <td>{job.id}</td>- <td>{job.updatedAt |> timeAgo}</td>- <td>{renderStatus job}</td>- <td><a href={ViewJobAction (job.table) (job.id)} class="text-primary">Show</a></td>- <td>- <form action={RetryJobAction (job.table) (job.id)} method="POST">- <button type="submit" style={retryButtonStyle} class="btn btn-link text-secondary">Retry</button>- </form>- </td>- </tr>- |]---- | Link included in table to create a new job.-renderNewBaseJobLink :: Text -> Html-renderNewBaseJobLink table =- let- link = "/jobs/CreateJob?tableName=" <> table- in [hsx|- <form action={link}>- <button type="submit" class="btn btn-primary btn-sm">+ New Job</button>- </form>- |]--renderNewBaseJobForm :: Text -> Html-renderNewBaseJobForm table = [hsx|- <br>- <h5>New Job: {table}</h5>- <br>- <form action="/jobs/CreateJob" method="POST">- <input type="hidden" id="tableName" name="tableName" value={table}>- <button type="submit" class="btn btn-primary">New Job</button>- </form>-|]--renderBaseJobDetailView :: BaseJob -> Html-renderBaseJobDetailView job = let table = job.table in [hsx|- <br>- <h5>Viewing Job {job.id} in {table |> columnNameToFieldLabel}</h5>- <br>- <table class="table">- <tbody>- <tr>- <th>Updated At</th>- <td>{job.updatedAt |> timeAgo} ({job.updatedAt})</td>- </tr>- <tr>- <th>Created At</th>- <td>{job.createdAt |> timeAgo} ({job.createdAt})</td>- </tr>- <tr>- <th>Status</th>- <td>{renderStatus job}</td>- </tr>- <tr>- <th>Last Error</th>- <td>{fromMaybe "No error" (job.lastError)}</td>- </tr>- </tbody>- </table>-- <div class="d-flex flex-row">- <form class="mr-2" action="/jobs/DeleteJob" method="POST">- <input type="hidden" id="tableName" name="tableName" value={table}>- <input type="hidden" id="id" name="id" value={tshow $ job.id}>- <button type="submit" class="btn btn-danger">Delete</button>- </form>- <form action="/jobs/RetryJob" method="POST">- <input type="hidden" id="tableName" name="tableName" value={table}>- <input type="hidden" id="id" name="id" value={tshow $ job.id}>- <button type="submit" class="btn btn-primary">Run again</button>- </form>- </div>-|]----------------------------------------------------------------------- TABLE VIEWABLE view helpers ------------------------------------makeDashboardSectionFromTableViewable :: forall a. (TableViewable a- , ?context :: ControllerContext- , ?modelContext :: ModelContext) => IO SomeView-makeDashboardSectionFromTableViewable = do- indexRows <- getIndex @a- pure $ SomeView $ HtmlView $ renderTableViewableTable indexRows--renderTableViewableTable :: forall a. TableViewable a => [a] -> Html-renderTableViewableTable rows = let- headers = tableHeaders @a- title = tableTitle @a- link = newJobLink @a- renderRow = renderTableRow @a- table = modelTableName @a- in [hsx|- <div>- <div class="d-flex justify-content-between align-items-center">- <h3>{title}</h3>- {link}- </div>- <table class="table table-sm table-hover">- <thead>- <tr>- {forEach headers renderHeader}- </tr>- </thead>-- <tbody>- {forEach rows renderRow}- </tbody>- </table>- <a href={ListJobAction table 1} class="link-primary">See all {title}</a>- <hr />- </div>-|]- where renderHeader field = [hsx|<th>{field}</th>|]----makeListPageFromTableViewable :: forall a. (TableViewable a, ?context :: ControllerContext, ?modelContext :: ModelContext) => Int -> Int -> IO SomeView-makeListPageFromTableViewable page pageSize = do- pageData <- getPage @a (page - 1) pageSize- numPages <- numberOfPagesForTable (modelTableName @a) pageSize- pure $ SomeView $ HtmlView $ renderTableViewableTablePaginated pageData page numPages--renderTableViewableTablePaginated :: forall a. TableViewable a => [a] -> Int -> Int -> Html-renderTableViewableTablePaginated jobs page totalPages =- let- title = tableTitle @a- table = modelTableName @a- headers = tableHeaders @a- lastJobIndex = (List.length jobs) - 1- newLink = newJobLink @a- in- [hsx|- <div>- <div class="d-flex justify-content-between align-items-center">- <h3>{title}</h3>- {newLink}- </div>- <table class="table table-sm table-hover">- <thead>- <tr>- {forEach headers renderHeader}- </tr>- </thead>-- <tbody>- {forEach jobs renderTableRow}- </tbody>- </table>- </div>- <nav aria-label="Page navigation example">- <ul class="pagination justify-content-end">- {renderPrev}- {when (totalPages /= 1) renderDest}- {renderNext}- </ul>- </nav>- |]- where- renderHeader field = [hsx|<th>{field}</th>|]- renderDest = let table = modelTableName @a in [hsx|<li class="page-item active"><a class="page-link" href={ListJobAction table page}>{page}</a></li>|]- renderPrev- | page == 1 = [hsx||]- | otherwise = let table = modelTableName @a in [hsx|- <li class="page-item">- <a class="page-link" href={ListJobAction table (page - 1)} aria-label="Previous">- <span aria-hidden="true">«</span>- <span class="sr-only">Previous</span>- </a>- </li>- |]- renderNext- | page == totalPages || totalPages == 0 = [hsx||]- | otherwise = let table = modelTableName @a in [hsx|- <li class="page-item">- <a class="page-link" href={ListJobAction table (page + 1)} aria-label="Next">- <span aria-hidden="true">»</span>- <span class="sr-only">Next</span>- </a>- </li>- |]---------------------------------------------------------------retryButtonStyle :: Text-retryButtonStyle = "outline: none !important; padding: 0; border: 0; vertical-align: baseline;"
@@ -1,269 +1,44 @@-{-|-Module: IHP.Job.Queue-Description: Functions to operate on the Job Queue Database-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.Job.Queue where--import IHP.Prelude-import IHP.Job.Types-import qualified Database.PostgreSQL.Simple as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Database.PostgreSQL.Simple.FromField as PG-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified Control.Concurrent.Async as Async-import qualified Control.Concurrent as Concurrent-import IHP.ModelSupport-import IHP.QueryBuilder-import IHP.Fetch-import IHP.Controller.Param-import qualified System.Random as Random-import qualified IHP.PGListener as PGListener-import qualified IHP.Log as Log-import Control.Monad.Trans.Resource---- | Lock and fetch the next available job. In case no job is available returns Nothing.------ The lock is set on the job row in an atomic way.--- --- The job status is set to JobStatusRunning, lockedBy will be set to the worker id and the attemptsCount is incremented.------ __Example:__ Locking a SendMailJob------ > let workerId :: UUID = "faa5ba30-1d76-4adf-bf01-2d1f95cddc04"--- > job <- fetchNextJob @SendMailJob workerId------ After you're done with the job, call 'jobDidFail' or 'jobDidSucceed' to make it available to the queue again.-fetchNextJob :: forall job.- ( ?modelContext :: ModelContext- , job ~ GetModelByTableName (GetTableName job)- , FilterPrimaryKey (GetTableName job)- , FromRow job- , Show (PrimaryKey (GetTableName job))- , PG.FromField (PrimaryKey (GetTableName job))- , Table job- ) => Maybe Int -> BackoffStrategy -> UUID -> IO (Maybe job)-fetchNextJob timeoutInMicroseconds backoffStrategy workerId = do- let query = PG.Query ("UPDATE ? SET status = ?, locked_at = NOW(), locked_by = ?, attempts_count = attempts_count + 1 WHERE id IN (SELECT id FROM ? WHERE (((status = ?) OR (status = ? AND " <> retryQuery backoffStrategy <> ")) AND locked_by IS NULL AND run_at <= NOW()) " <> timeoutCondition timeoutInMicroseconds <> " ORDER BY created_at LIMIT 1 FOR UPDATE) RETURNING id")- let params = (PG.Identifier (tableName @job), JobStatusRunning, workerId, PG.Identifier (tableName @job), JobStatusNotStarted, JobStatusRetry, backoffStrategy.delayInSeconds, timeoutInMicroseconds)-- result :: [PG.Only (Id job)] <- withoutQueryLogging (sqlQuery query params)- case result of- [] -> pure Nothing- [PG.Only id] -> Just <$> withoutQueryLogging (fetch id)- otherwise -> error (show otherwise)---- | Calls a callback every time something is inserted, updated or deleted in a given database table.------ In the background this function creates a database trigger to notify this function about table changes--- using pg_notify. When there are existing triggers, it will silently recreate them. So this will most likely--- not fail.------ This function returns a Async. Call 'cancel' on the async to stop watching the database.------ __Example:__------ > watchInsertOrUpdateTable "projects" do--- > putStrLn "Something changed in the projects table"------ Now insert something into the @projects@ table. E.g. by running @make psql@ and then running @INSERT INTO projects (id, name) VALUES (DEFAULT, 'New project');@--- You will see that @"Something changed in the projects table"@ is printed onto the screen.----watchForJob :: (?modelContext :: ModelContext) => PGListener.PGListener -> Text -> Int -> Maybe Int -> BackoffStrategy -> Concurrent.MVar JobWorkerProcessMessage -> ResourceT IO (PGListener.Subscription, ReleaseKey)-watchForJob pgListener tableName pollInterval timeoutInMicroseconds backoffStrategy onNewJob = do- let tableNameBS = cs tableName- liftIO $ withoutQueryLogging (sqlExec (createNotificationTrigger tableNameBS) ())-- poller <- pollForJob tableName pollInterval timeoutInMicroseconds backoffStrategy onNewJob- subscription <- liftIO $ pgListener |> PGListener.subscribe (channelName tableNameBS) (const (Concurrent.putMVar onNewJob JobAvailable))-- pure (subscription, poller)---- | Periodically checks the queue table for open jobs. Calls the callback if there are any.------ 'watchForJob' only catches jobs when something is changed on the table. When a job is scheduled--- with a 'runAt' in the future, and no other operation is happening on the queue, the database triggers--- will not run, and so 'watchForJob' cannot pick up the job even when 'runAt' is now in the past.------ This function returns a Async. Call 'cancel' on the async to stop polling the database.----pollForJob :: (?modelContext :: ModelContext) => Text -> Int -> Maybe Int -> BackoffStrategy -> Concurrent.MVar JobWorkerProcessMessage -> ResourceT IO ReleaseKey-pollForJob tableName pollInterval timeoutInMicroseconds backoffStrategy onNewJob = do- let query = PG.Query ("SELECT COUNT(*) FROM ? WHERE (((status = ?) OR (status = ? AND " <> retryQuery backoffStrategy <> ")) AND locked_by IS NULL AND run_at <= NOW()) " <> timeoutCondition timeoutInMicroseconds <> " LIMIT 1")- let params = (PG.Identifier tableName, JobStatusNotStarted, JobStatusRetry, backoffStrategy.delayInSeconds, timeoutInMicroseconds)- let handler = do- forever do- -- We don't log the queries to the console as it's filling up the log entries with noise- count :: Int <- withoutQueryLogging (sqlQueryScalar query params)-- -- For every job we send one signal to the job workers- -- This way we use full concurrency when we find multiple jobs- -- that haven't been picked up by the PGListener- forEach [1..count] \_ -> do- Concurrent.putMVar onNewJob JobAvailable-- -- Add up to 2 seconds of jitter to avoid all job queues polling at the same time- jitter <- Random.randomRIO (0, 2000000)- let pollIntervalWithJitter = pollInterval + jitter-- Concurrent.threadDelay pollIntervalWithJitter-- fst <$> allocate (Async.async handler) Async.cancel--createNotificationTrigger :: ByteString -> PG.Query-createNotificationTrigger tableName = PG.Query $ ""- <> "BEGIN;\n"- <> "CREATE OR REPLACE FUNCTION " <> functionName <> "() RETURNS TRIGGER AS $$"- <> "BEGIN\n"- <> " PERFORM pg_notify('" <> channelName tableName <> "', '');\n"- <> " RETURN new;"- <> "END;\n"- <> "$$ language plpgsql;"- <> "DROP TRIGGER IF EXISTS " <> insertTriggerName <> " ON " <> tableName <> "; CREATE TRIGGER " <> insertTriggerName <> " AFTER INSERT ON \"" <> tableName <> "\" FOR EACH ROW WHEN (NEW.status = 'job_status_not_started' OR NEW.status = 'job_status_retry') EXECUTE PROCEDURE " <> functionName <> "();\n"- <> "DROP TRIGGER IF EXISTS " <> updateTriggerName <> " ON " <> tableName <> "; CREATE TRIGGER " <> updateTriggerName <> " AFTER UPDATE ON \"" <> tableName <> "\" FOR EACH ROW WHEN (NEW.status = 'job_status_not_started' OR NEW.status = 'job_status_retry') EXECUTE PROCEDURE " <> functionName <> "();\n"- <> "COMMIT;"- where- functionName = "notify_job_queued_" <> tableName- insertTriggerName = "did_insert_job_" <> tableName- updateTriggerName = "did_update_job_" <> tableName---- | Retuns the event name of the event that the pg notify trigger dispatches-channelName :: ByteString -> ByteString-channelName tableName = "job_available_" <> tableName---- | Called when a job failed. Sets the job status to 'JobStatusFailed' or 'JobStatusRetry' (if more attempts are possible) and resets 'lockedBy'-jobDidFail :: forall job context.- ( job ~ GetModelByTableName (GetTableName job)- , SetField "lockedBy" job (Maybe UUID)- , SetField "status" job JobStatus- , SetField "updatedAt" job UTCTime- , HasField "attemptsCount" job Int- , SetField "lastError" job (Maybe Text)- , Job job- , CanUpdate job- , Show job- , ?modelContext :: ModelContext- , ?context :: context- , HasField "logger" context Log.Logger- ) => job -> SomeException -> IO ()-jobDidFail job exception = do- updatedAt <- getCurrentTime-- Log.warn ("Failed job with exception: " <> tshow exception)-- let ?job = job- let canRetry = job.attemptsCount < maxAttempts- let status = if canRetry then JobStatusRetry else JobStatusFailed- job- |> set #status status- |> set #lockedBy Nothing- |> set #updatedAt updatedAt- |> set #lastError (Just (tshow exception))- |> updateRecord-- pure ()--jobDidTimeout :: forall job context.- ( job ~ GetModelByTableName (GetTableName job)- , SetField "lockedBy" job (Maybe UUID)- , SetField "status" job JobStatus- , SetField "updatedAt" job UTCTime- , HasField "attemptsCount" job Int- , SetField "lastError" job (Maybe Text)- , Job job- , CanUpdate job- , Show job- , ?modelContext :: ModelContext- , ?context :: context- , HasField "logger" context Log.Logger- ) => job -> IO ()-jobDidTimeout job = do- updatedAt <- getCurrentTime-- Log.warn ("Job timed out" :: Text)-- let ?job = job- let canRetry = job.attemptsCount < maxAttempts- let status = if canRetry then JobStatusRetry else JobStatusTimedOut- job- |> set #status status- |> set #lockedBy Nothing- |> set #updatedAt updatedAt- |> setJust #lastError "Timeout reached"- |> updateRecord-- pure ()- ---- | Called when a job succeeded. Sets the job status to 'JobStatusSucceded' and resets 'lockedBy'-jobDidSucceed :: forall job context.- ( job ~ GetModelByTableName (GetTableName job)- , SetField "lockedBy" job (Maybe UUID)- , SetField "status" job JobStatus- , SetField "updatedAt" job UTCTime- , HasField "attemptsCount" job Int- , SetField "lastError" job (Maybe Text)- , Job job- , CanUpdate job- , Show job- , ?modelContext :: ModelContext- , ?context :: context- , HasField "logger" context Log.Logger- ) => job -> IO ()-jobDidSucceed job = do- Log.info ("Succeeded job" :: Text)- updatedAt <- getCurrentTime- job- |> set #status JobStatusSucceeded- |> set #lockedBy Nothing- |> set #updatedAt updatedAt- |> updateRecord-- pure ()---- | Mapping for @JOB_STATUS@:------ > CREATE TYPE JOB_STATUS AS ENUM ('job_status_not_started', 'job_status_running', 'job_status_failed', 'job_status_succeeded', 'job_status_retry');-instance PG.FromField JobStatus where- fromField field (Just "job_status_not_started") = pure JobStatusNotStarted- fromField field (Just "job_status_running") = pure JobStatusRunning- fromField field (Just "job_status_failed") = pure JobStatusFailed- fromField field (Just "job_status_timed_out") = pure JobStatusTimedOut- fromField field (Just "job_status_succeeded") = pure JobStatusSucceeded- fromField field (Just "job_status_retry") = pure JobStatusRetry- fromField field (Just value) = PG.returnError PG.ConversionFailed field ("Unexpected value for enum value. Got: " <> cs value)- fromField field Nothing = PG.returnError PG.UnexpectedNull field "Unexpected null for enum value"---- The default state is @not started@-instance Default JobStatus where- def = JobStatusNotStarted---- | Mapping for @JOB_STATUS@:------ > CREATE TYPE JOB_STATUS AS ENUM ('job_status_not_started', 'job_status_running', 'job_status_failed', 'job_status_succeeded', 'job_status_retry');-instance PG.ToField JobStatus where- toField JobStatusNotStarted = PG.toField ("job_status_not_started" :: Text)- toField JobStatusRunning = PG.toField ("job_status_running" :: Text)- toField JobStatusFailed = PG.toField ("job_status_failed" :: Text)- toField JobStatusTimedOut = PG.toField ("job_status_timed_out" :: Text)- toField JobStatusSucceeded = PG.toField ("job_status_succeeded" :: Text)- toField JobStatusRetry = PG.toField ("job_status_retry" :: Text)--instance InputValue JobStatus where- inputValue JobStatusNotStarted = "job_status_not_started" :: Text- inputValue JobStatusRunning = "job_status_running" :: Text- inputValue JobStatusFailed = "job_status_failed" :: Text- inputValue JobStatusTimedOut = "job_status_timed_out" :: Text- inputValue JobStatusSucceeded = "job_status_succeeded" :: Text- inputValue JobStatusRetry = "job_status_retry" :: Text--instance IHP.Controller.Param.ParamReader JobStatus where- readParameter = IHP.Controller.Param.enumParamReader--retryQuery :: BackoffStrategy -> ByteString-retryQuery LinearBackoff {} = "updated_at < NOW() + (interval '1 second' * ?)"-retryQuery ExponentialBackoff {} = "updated_at < NOW() - interval '1 second' * ? * POW(2, attempts_count)"+module IHP.Job.Queue+( runPool+, fetchNextJob+, pendingJobConditionSQL+, watchForJob+, watchForJobWithPollerTriggerRepair+, pollForJob+, notificationTriggersHealthy+, ensureNotificationTriggers+, createNotificationTriggerSQL+, channelName+, jobDidFail+, jobDidTimeout+, jobDidSucceed+, backoffDelay+, recoverStaleJobs+, textToEnumJobStatusMap+, textToEnumJobStatus+, tryWriteTBQueue+) where -timeoutCondition :: Maybe Int -> ByteString-timeoutCondition (Just timeoutInMicroseconds) = "OR (status = 'job_status_running' AND locked_by IS NOT NULL AND locked_at + ((? + 1000000) || 'microseconds')::interval < NOW())" -- Add 1000000 here to avoid race condition with the Haskell based timeout mechanism-timeoutCondition Nothing = "AND (? IS NULL)"+import IHP.Job.Queue.Pool (runPool)+import IHP.Job.Queue.Fetch (fetchNextJob, pendingJobConditionSQL)+import IHP.Job.Queue.Watch+ ( watchForJob+ , watchForJobWithPollerTriggerRepair+ , pollForJob+ , notificationTriggersHealthy+ , ensureNotificationTriggers+ , createNotificationTriggerSQL+ , channelName+ )+import IHP.Job.Queue.Result+ ( jobDidFail+ , jobDidTimeout+ , jobDidSucceed+ , backoffDelay+ , recoverStaleJobs+ )+import IHP.Job.Queue.StatusInstances+ ( textToEnumJobStatusMap+ , textToEnumJobStatus+ )+import IHP.Job.Queue.STM (tryWriteTBQueue)
@@ -0,0 +1,61 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+module IHP.Job.Queue.Fetch+( fetchNextJob+, pendingJobConditionSQL+) where++import IHP.Prelude+import IHP.Job.Queue.Pool (runPool)+import IHP.ModelSupport (Table (..), GetModelByTableName)+import IHP.ModelSupport.Types (PrimaryKey)+import IHP.Hasql.FromRow (FromRowHasql (..))+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Session as HasqlSession+import qualified Hasql.Statement as Hasql+import qualified Hasql.Encoders as Encoders+import qualified Hasql.Decoders as Decoders+import qualified Data.Text as Text++-- | Lock and fetch the next available job. In case no job is available returns Nothing.+--+-- The lock is set on the job row in an atomic way.+--+-- The job status is set to JobStatusRunning, lockedBy will be set to the worker id and the attemptsCount is incremented.+--+-- __Example:__ Locking a SendMailJob+--+-- > let workerId :: UUID = "faa5ba30-1d76-4adf-bf01-2d1f95cddc04"+-- > job <- fetchNextJob @SendMailJob pool workerId+--+-- After you're done with the job, call 'jobDidFail' or 'jobDidSucceed' to make it available to the queue again.+fetchNextJob :: forall job.+ ( job ~ GetModelByTableName (GetTableName job)+ , FromRowHasql job+ , Show (PrimaryKey (GetTableName job))+ , Table job+ ) => HasqlPool.Pool -> UUID -> IO (Maybe job)+fetchNextJob pool workerId = do+ let tableNameText = tableName @job+ let returningColumns = Text.intercalate ", " (columnNames @job)+ let sql = "UPDATE " <> tableNameText+ <> " SET status = 'job_status_running'"+ <> ", locked_at = NOW(), locked_by = $1"+ <> ", attempts_count = attempts_count + 1"+ <> " WHERE id IN (SELECT id FROM " <> tableNameText+ <> " WHERE " <> pendingJobConditionSQL+ <> " ORDER BY created_at LIMIT 1 FOR UPDATE SKIP LOCKED)"+ <> " RETURNING " <> returningColumns+ let encoder = Encoders.param (Encoders.nonNullable Encoders.uuid)+ let decoder = Decoders.rowMaybe (hasqlRowDecoder @job)+ let statement = Hasql.unpreparable sql encoder decoder+ runPool pool (HasqlSession.statement workerId statement)++-- | Shared WHERE condition for fetching pending jobs as a SQL text fragment.+-- Matches jobs that are either not started or in retry state,+-- not locked, and whose run_at time has passed.+-- Enum values are inlined as SQL string literals (PostgreSQL casts them to job_status).+pendingJobConditionSQL :: Text+pendingJobConditionSQL =+ "(status = 'job_status_not_started'"+ <> " OR status = 'job_status_retry'"+ <> ") AND locked_by IS NULL AND run_at <= NOW()"
@@ -0,0 +1,12 @@+module IHP.Job.Queue.Pool+( runPool+) where++import IHP.Prelude+import IHP.Hasql.Pool (usePoolWithRetry)+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Session as HasqlSession++-- | Run a hasql session against the pool, retrying once on cached plan errors.+runPool :: HasqlPool.Pool -> HasqlSession.Session a -> IO a+runPool = usePoolWithRetry
@@ -0,0 +1,156 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+module IHP.Job.Queue.Result+( jobDidFail+, jobDidTimeout+, jobDidSucceed+, backoffDelay+, recoverStaleJobs+) where++import IHP.Prelude+import IHP.Job.Types+import IHP.Job.Queue.Pool (runPool)+import IHP.Job.Queue.StatusInstances ()+import IHP.ModelSupport (Table (..), InputValue (..))+import IHP.ModelSupport.Types (Id' (..), PrimaryKey)+import qualified IHP.Log as Log+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Session as HasqlSession+import qualified Hasql.Statement as Hasql+import qualified Hasql.Encoders as Encoders+import qualified Hasql.Decoders as Decoders+import Data.Functor.Contravariant (contramap)++-- | Called when a job failed. Sets the job status to 'JobStatusFailed' or 'JobStatusRetry' (if more attempts are possible) and resets 'lockedBy'+jobDidFail :: forall job context.+ ( Table job+ , HasField "id" job (Id' (GetTableName job))+ , PrimaryKey (GetTableName job) ~ UUID+ , HasField "attemptsCount" job Int+ , HasField "runAt" job UTCTime+ , Job job+ , ?context :: context+ , HasField "logger" context Log.Logger+ ) => HasqlPool.Pool -> job -> SomeException -> IO ()+jobDidFail pool job exception = do+ now <- getCurrentTime++ Log.warn ("Failed job with exception: " <> tshow exception)++ let ?job = job+ let canRetry = job.attemptsCount < maxAttempts+ let status = if canRetry then JobStatusRetry else JobStatusFailed+ let nextRunAt = if canRetry+ then addUTCTime (backoffDelay (backoffStrategy @job) job.attemptsCount) now+ else job.runAt+ let Id jobId = job.id+ let tableNameText = tableName @job+ let sql = "UPDATE " <> tableNameText+ <> " SET status = $1::public.job_status, locked_by = NULL, updated_at = $2, last_error = $3, run_at = $4 WHERE id = $5"+ let encoder =+ contramap (\(s,_,_,_,_) -> s) (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap (\(_,u,_,_,_) -> u) (Encoders.param (Encoders.nonNullable Encoders.timestamptz))+ <> contramap (\(_,_,e,_,_) -> e) (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap (\(_,_,_,r,_) -> r) (Encoders.param (Encoders.nonNullable Encoders.timestamptz))+ <> contramap (\(_,_,_,_,i) -> i) (Encoders.param (Encoders.nonNullable Encoders.uuid))+ let statement = Hasql.unpreparable sql encoder Decoders.noResult+ runPool pool (HasqlSession.statement (inputValue status, now, tshow exception, nextRunAt, jobId) statement)++jobDidTimeout :: forall job context.+ ( Table job+ , HasField "id" job (Id' (GetTableName job))+ , PrimaryKey (GetTableName job) ~ UUID+ , HasField "attemptsCount" job Int+ , HasField "runAt" job UTCTime+ , Job job+ , ?context :: context+ , HasField "logger" context Log.Logger+ ) => HasqlPool.Pool -> job -> IO ()+jobDidTimeout pool job = do+ now <- getCurrentTime++ Log.warn ("Job timed out" :: Text)++ let ?job = job+ let canRetry = job.attemptsCount < maxAttempts+ let status = if canRetry then JobStatusRetry else JobStatusTimedOut+ let nextRunAt = if canRetry+ then addUTCTime (backoffDelay (backoffStrategy @job) job.attemptsCount) now+ else job.runAt+ let Id jobId = job.id+ let tableNameText = tableName @job+ let sql = "UPDATE " <> tableNameText+ <> " SET status = $1::public.job_status, locked_by = NULL, updated_at = $2, last_error = $3, run_at = $4 WHERE id = $5"+ let encoder =+ contramap (\(s,_,_,_,_) -> s) (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap (\(_,u,_,_,_) -> u) (Encoders.param (Encoders.nonNullable Encoders.timestamptz))+ <> contramap (\(_,_,e,_,_) -> e) (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap (\(_,_,_,r,_) -> r) (Encoders.param (Encoders.nonNullable Encoders.timestamptz))+ <> contramap (\(_,_,_,_,i) -> i) (Encoders.param (Encoders.nonNullable Encoders.uuid))+ let statement = Hasql.unpreparable sql encoder Decoders.noResult+ runPool pool (HasqlSession.statement (inputValue status, now, "Timeout reached" :: Text, nextRunAt, jobId) statement)+++-- | Called when a job succeeded. Sets the job status to 'JobStatusSucceded' and resets 'lockedBy'+jobDidSucceed :: forall job context.+ ( Table job+ , HasField "id" job (Id' (GetTableName job))+ , PrimaryKey (GetTableName job) ~ UUID+ , ?context :: context+ , HasField "logger" context Log.Logger+ ) => HasqlPool.Pool -> job -> IO ()+jobDidSucceed pool job = do+ Log.info ("Succeeded job" :: Text)+ updatedAt <- getCurrentTime+ let Id jobId = job.id+ let tableNameText = tableName @job+ let sql = "UPDATE " <> tableNameText+ <> " SET status = 'job_status_succeeded', locked_by = NULL, updated_at = $1 WHERE id = $2"+ let encoder =+ contramap fst (Encoders.param (Encoders.nonNullable Encoders.timestamptz))+ <> contramap snd (Encoders.param (Encoders.nonNullable Encoders.uuid))+ let statement = Hasql.unpreparable sql encoder Decoders.noResult+ runPool pool (HasqlSession.statement (updatedAt, jobId) statement)++-- | Compute the delay before the next retry attempt.+--+-- For 'LinearBackoff', the delay is constant.+-- For 'ExponentialBackoff', the delay doubles each attempt, capped at 24 hours.+backoffDelay :: BackoffStrategy -> Int -> NominalDiffTime+backoffDelay (LinearBackoff { delayInSeconds }) _ = fromIntegral delayInSeconds+backoffDelay (ExponentialBackoff { delayInSeconds }) attempts =+ min 86400 (fromIntegral delayInSeconds * (2 ^ min attempts 20))++-- | Recover stale jobs that have been in 'JobStatusRunning' for too long,+-- likely due to a worker crash.+--+-- Two-tier recovery:+-- - Recently stale jobs (within 24h) are set back to retry+-- - Ancient stale jobs (older than 24h) are marked as failed+recoverStaleJobs :: forall job.+ ( Table job+ ) => HasqlPool.Pool -> NominalDiffTime -> IO ()+recoverStaleJobs pool staleThreshold = do+ let tableNameText = tableName @job+ -- Tier 1: Recently stale jobs (threshold..24h) -> retry+ let retrySql =+ "UPDATE " <> tableNameText+ <> " SET status = 'job_status_retry', locked_by = NULL, locked_at = NULL, run_at = NOW()"+ <> " WHERE status = 'job_status_running'"+ <> " AND locked_at < NOW() - interval '1 second' * $1"+ <> " AND locked_at > NOW() - interval '1 day'"+ let retryEncoder = Encoders.param (Encoders.nonNullable (contramap (fromIntegral :: Int -> Int64) Encoders.int8))+ let retryStatement = Hasql.unpreparable retrySql retryEncoder Decoders.noResult++ -- Tier 2: Ancient stale jobs (>24h) -> mark failed+ let failSql =+ "UPDATE " <> tableNameText+ <> " SET status = 'job_status_failed', locked_by = NULL, locked_at = NULL"+ <> ", last_error = 'Stale job: worker likely crashed'"+ <> " WHERE status = 'job_status_running'"+ <> " AND locked_at < NOW() - interval '1 day'"+ let failStatement = Hasql.unpreparable failSql Encoders.noParams Decoders.noResult++ let thresholdSeconds = round staleThreshold :: Int+ runPool pool (HasqlSession.statement thresholdSeconds retryStatement)+ runPool pool (HasqlSession.statement () failStatement)
@@ -0,0 +1,18 @@+module IHP.Job.Queue.STM+( tryWriteTBQueue+) where++import IHP.Prelude+import Control.Concurrent.STM (TBQueue, writeTBQueue, STM)+import Control.Concurrent.STM.TBQueue (isFullTBQueue)++-- | Non-blocking write to a TBQueue. Returns True if the value was written,+-- False if the queue was full.+tryWriteTBQueue :: TBQueue a -> a -> STM Bool+tryWriteTBQueue queue value = do+ full <- isFullTBQueue queue+ if full+ then pure False+ else do+ writeTBQueue queue value+ pure True
@@ -0,0 +1,83 @@+module IHP.Job.Queue.StatusInstances+( textToEnumJobStatusMap+, textToEnumJobStatus+) where++import IHP.Prelude+import IHP.Job.Types (JobStatus (..))+import qualified Database.PostgreSQL.Simple.FromField as PG+import qualified Database.PostgreSQL.Simple.ToField as PG+import IHP.ModelSupport (InputValue (..))+import qualified IHP.Controller.Param as Param+import qualified Data.HashMap.Strict as HashMap+import qualified Hasql.Encoders as Encoders+import qualified Hasql.Decoders as Decoders+import Hasql.Implicits.Encoders (DefaultParamEncoder (..))+import qualified Hasql.Mapping.IsScalar as Mapping++-- | Mapping for @JOB_STATUS@:+--+-- > CREATE TYPE JOB_STATUS AS ENUM ('job_status_not_started', 'job_status_running', 'job_status_failed', 'job_status_succeeded', 'job_status_retry');+--+-- These instances are needed by the generated @FromRow@ instances in user apps+-- (see 'compileFromRowInstance' in "IHP.SchemaCompiler").+instance PG.FromField JobStatus where+ fromField field (Just "job_status_not_started") = pure JobStatusNotStarted+ fromField field (Just "job_status_running") = pure JobStatusRunning+ fromField field (Just "job_status_failed") = pure JobStatusFailed+ fromField field (Just "job_status_timed_out") = pure JobStatusTimedOut+ fromField field (Just "job_status_succeeded") = pure JobStatusSucceeded+ fromField field (Just "job_status_retry") = pure JobStatusRetry+ fromField field (Just value) = PG.returnError PG.ConversionFailed field ("Unexpected value for enum value. Got: " <> cs value)+ fromField field Nothing = PG.returnError PG.UnexpectedNull field "Unexpected null for enum value"++-- The default state is @not started@+instance Default JobStatus where+ def = JobStatusNotStarted++-- | See 'FromField' instance above.+instance PG.ToField JobStatus where+ toField JobStatusNotStarted = PG.toField ("job_status_not_started" :: Text)+ toField JobStatusRunning = PG.toField ("job_status_running" :: Text)+ toField JobStatusFailed = PG.toField ("job_status_failed" :: Text)+ toField JobStatusTimedOut = PG.toField ("job_status_timed_out" :: Text)+ toField JobStatusSucceeded = PG.toField ("job_status_succeeded" :: Text)+ toField JobStatusRetry = PG.toField ("job_status_retry" :: Text)++instance InputValue JobStatus where+ inputValue JobStatusNotStarted = "job_status_not_started" :: Text+ inputValue JobStatusRunning = "job_status_running" :: Text+ inputValue JobStatusFailed = "job_status_failed" :: Text+ inputValue JobStatusTimedOut = "job_status_timed_out" :: Text+ inputValue JobStatusSucceeded = "job_status_succeeded" :: Text+ inputValue JobStatusRetry = "job_status_retry" :: Text++instance Param.ParamReader JobStatus where+ readParameter = Param.enumParamReader++-- | Parses a Text value to a JobStatus. Used by hasql decoders.+-- Uses HashMap for O(1) lookup.+textToEnumJobStatusMap :: HashMap.HashMap Text JobStatus+textToEnumJobStatusMap = HashMap.fromList+ [ ("job_status_not_started", JobStatusNotStarted)+ , ("job_status_running", JobStatusRunning)+ , ("job_status_failed", JobStatusFailed)+ , ("job_status_timed_out", JobStatusTimedOut)+ , ("job_status_succeeded", JobStatusSucceeded)+ , ("job_status_retry", JobStatusRetry)+ ]++textToEnumJobStatus :: Text -> Maybe JobStatus+textToEnumJobStatus t = HashMap.lookup t textToEnumJobStatusMap++-- | DefaultParamEncoder for hasql queries using JobStatus in filterWhere+instance DefaultParamEncoder JobStatus where+ defaultParam = Encoders.nonNullable (Encoders.enum (Just "public") "job_status" inputValue)++-- | DefaultParamEncoder for lists of JobStatus, needed for filterWhereIn/filterWhereNotIn+instance DefaultParamEncoder [JobStatus] where+ defaultParam = Encoders.nonNullable $ Encoders.foldableArray $ Encoders.nonNullable (Encoders.enum (Just "public") "job_status" inputValue)++instance Mapping.IsScalar JobStatus where+ encoder = Encoders.enum (Just "public") "job_status" inputValue+ decoder = Decoders.enum (Just "public") "job_status" textToEnumJobStatus
@@ -0,0 +1,176 @@+module IHP.Job.Queue.Watch+( watchForJob+, watchForJobWithPollerTriggerRepair+, pollForJob+, notificationTriggersHealthy+, ensureNotificationTriggers+, createNotificationTriggerSQL+, channelName+) where++import IHP.Prelude+import IHP.Job.Queue.Pool (runPool)+import IHP.Job.Queue.Fetch (pendingJobConditionSQL)+import IHP.Job.Queue.STM (tryWriteTBQueue)+import IHP.Job.Types (JobWorkerProcessMessage (..))+import qualified IHP.PGListener as PGListener+import qualified IHP.Log as Log+import Control.Monad.Trans.Resource+import qualified Control.Exception.Safe as Exception+import qualified Control.Concurrent as Concurrent+import qualified Control.Concurrent.Async as Async+import qualified System.Random as Random+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Session as HasqlSession+import qualified Hasql.Connection as HasqlConnection+import qualified Hasql.Statement as Hasql+import qualified Hasql.Encoders as Encoders+import qualified Hasql.Decoders as Decoders+import Control.Concurrent.STM (TBQueue, atomically)+import Data.Functor.Contravariant (contramap)++-- | Calls a callback every time something is inserted, updated or deleted in a given database table.+--+-- In the background this function creates a database trigger to notify this function about table changes+-- using pg_notify. When there are existing triggers, it will silently recreate them. So this will most likely+-- not fail.+--+-- This function returns a Async. Call 'cancel' on the async to stop watching the database.+--+-- __Example:__+--+-- > watchInsertOrUpdateTable "projects" do+-- > putStrLn "Something changed in the projects table"+--+-- Now insert something into the @projects@ table. E.g. by running @make psql@ and then running @INSERT INTO projects (id, name) VALUES (DEFAULT, 'New project');@+-- You will see that @"Something changed in the projects table"@ is printed onto the screen.+watchForJob :: (?context :: context, HasField "logger" context Log.Logger) => HasqlPool.Pool -> PGListener.PGListener -> Text -> Int -> TBQueue JobWorkerProcessMessage -> ResourceT IO (PGListener.Subscription, ReleaseKey)+watchForJob pool pgListener tableName pollInterval onNewJob =+ watchForJobWithPollerTriggerRepair False pool pgListener tableName pollInterval onNewJob++-- | Like 'watchForJob' but allows enabling a poller-side trigger integrity check.+-- Useful in development to recover from missing triggers after `make db`.+watchForJobWithPollerTriggerRepair :: (?context :: context, HasField "logger" context Log.Logger) => Bool -> HasqlPool.Pool -> PGListener.PGListener -> Text -> Int -> TBQueue JobWorkerProcessMessage -> ResourceT IO (PGListener.Subscription, ReleaseKey)+watchForJobWithPollerTriggerRepair enablePollerTriggerRepair pool pgListener tableName pollInterval onNewJob = do+ let tableNameBS = cs tableName+ liftIO do+ runPool pool (HasqlSession.script (createNotificationTriggerSQL tableNameBS))++ -- Recreate notification triggers when PGListener reconnects (e.g. after `make db` drops the database)+ PGListener.onReconnect (\connection -> do+ result <- HasqlConnection.use connection (HasqlSession.script (createNotificationTriggerSQL tableNameBS))+ case result of+ Left err -> Log.warn ("Failed to recreate notification triggers for " <> tableName <> ": " <> tshow err <> ". Falling back to poller.")+ Right _ -> Log.info ("Recreated notification triggers for " <> tableName)+ ) pgListener++ poller <- pollForJob enablePollerTriggerRepair pool tableName pollInterval onNewJob+ subscription <- liftIO $ pgListener |> PGListener.subscribe (channelName tableNameBS) (const (do+ Log.debug ("Received pg_notify for " <> tableName)+ didWrite <- atomically $ tryWriteTBQueue onNewJob JobAvailable+ unless didWrite (Log.warn ("Job queue full for " <> tableName))+ ))++ pure (subscription, poller)++-- | Periodically checks the queue table for open jobs. Calls the callback if there are any.+--+-- 'watchForJob' only catches jobs when something is changed on the table. When a job is scheduled+-- with a 'runAt' in the future, and no other operation is happening on the queue, the database triggers+-- will not run, and so 'watchForJob' cannot pick up the job even when 'runAt' is now in the past.+--+-- This function returns a Async. Call 'cancel' on the async to stop polling the database.+pollForJob :: (?context :: context, HasField "logger" context Log.Logger) => Bool -> HasqlPool.Pool -> Text -> Int -> TBQueue JobWorkerProcessMessage -> ResourceT IO ReleaseKey+pollForJob enablePollerTriggerRepair pool tableName pollInterval onNewJob = do+ let sql = "SELECT COUNT(*) FROM " <> tableName+ <> " WHERE " <> pendingJobConditionSQL+ let decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int8))+ let statement = Hasql.unpreparable sql Encoders.noParams decoder+ let handler = do+ forever do+ result <- Exception.tryAny do+ when enablePollerTriggerRepair do+ ensureNotificationTriggers pool tableName++ count :: Int <- fromIntegral <$> runPool pool (HasqlSession.statement () statement)++ -- For every job we send one signal to the job workers+ -- This way we use full concurrency when we find multiple jobs+ -- that haven't been picked up by the PGListener+ forEach [1..count] \_ -> do+ _ <- atomically $ tryWriteTBQueue onNewJob JobAvailable+ pure ()+ case result of+ Left exception -> Log.error ("Job poller: " <> tshow exception)+ Right _ -> pure ()++ -- Add up to 2 seconds of jitter to avoid all job queues polling at the same time+ jitter <- Random.randomRIO (0, 2000000)+ let pollIntervalWithJitter = pollInterval + jitter++ Concurrent.threadDelay pollIntervalWithJitter++ fst <$> allocate (Async.async handler) Async.cancel++notificationTriggersHealthy :: HasqlPool.Pool -> Text -> IO Bool+notificationTriggersHealthy pool tableName = do+ let insertTriggerName = "did_insert_job_" <> tableName+ let updateTriggerName = "did_update_job_" <> tableName+ let sql = "SELECT COUNT(*) FROM pg_trigger t"+ <> " JOIN pg_class c ON t.tgrelid = c.oid"+ <> " JOIN pg_namespace n ON c.relnamespace = n.oid"+ <> " WHERE n.nspname = current_schema()"+ <> " AND c.relname = $1::name"+ <> " AND NOT t.tgisinternal"+ <> " AND (t.tgname = $2::name OR t.tgname = $3::name)"+ let encoder =+ contramap (\(tableNameParam, _, _) -> tableNameParam) (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap (\(_, insertTriggerNameParam, _) -> insertTriggerNameParam) (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap (\(_, _, updateTriggerNameParam) -> updateTriggerNameParam) (Encoders.param (Encoders.nonNullable Encoders.text))+ let decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int8))+ let statement = Hasql.unpreparable sql encoder decoder+ count :: Int <- fromIntegral <$> runPool pool (HasqlSession.statement (tableName, insertTriggerName, updateTriggerName) statement)+ pure (count == 2)++ensureNotificationTriggers :: (?context :: context, HasField "logger" context Log.Logger) => HasqlPool.Pool -> Text -> IO ()+ensureNotificationTriggers pool tableName = do+ healthy <- notificationTriggersHealthy pool tableName+ unless healthy do+ let insertTriggerName = "did_insert_job_" <> tableName+ let updateTriggerName = "did_update_job_" <> tableName+ Log.warn ("Job poller: Missing notification triggers for " <> tableName <> " (" <> insertTriggerName <> ", " <> updateTriggerName <> "). Recreating.")+ runPool pool (HasqlSession.script (createNotificationTriggerSQL (cs tableName)))+ Log.info ("Job poller: Recreated notification triggers for " <> tableName)++-- | Returns a SQL script to create the notification trigger.+--+-- Wrapped in a DO $$ block with EXCEPTION handler because concurrent requests+-- can race to CREATE OR REPLACE the same function, causing PostgreSQL to throw+-- 'tuple concurrently updated' (SQLSTATE XX000). This is safe to ignore: the+-- other connection's CREATE OR REPLACE will have succeeded.+createNotificationTriggerSQL :: ByteString -> Text+createNotificationTriggerSQL tableName =+ cs $+ "DO $$\n"+ <> "BEGIN\n"+ <> " CREATE OR REPLACE FUNCTION " <> functionName <> "() RETURNS TRIGGER AS $BODY$"+ <> "BEGIN\n"+ <> " PERFORM pg_notify('" <> channelName tableName <> "', '');\n"+ <> " RETURN new;"+ <> "\nEND;\n"+ <> "$BODY$ language plpgsql;\n"+ <> " DROP TRIGGER IF EXISTS " <> insertTriggerName <> " ON " <> tableName <> ";\n"+ <> " CREATE TRIGGER " <> insertTriggerName <> " AFTER INSERT ON \"" <> tableName <> "\" FOR EACH ROW WHEN (NEW.status = 'job_status_not_started' OR NEW.status = 'job_status_retry') EXECUTE PROCEDURE " <> functionName <> "();\n"+ <> " DROP TRIGGER IF EXISTS " <> updateTriggerName <> " ON " <> tableName <> ";\n"+ <> " CREATE TRIGGER " <> updateTriggerName <> " AFTER UPDATE ON \"" <> tableName <> "\" FOR EACH ROW WHEN (NEW.status = 'job_status_not_started' OR NEW.status = 'job_status_retry') EXECUTE PROCEDURE " <> functionName <> "();\n"+ <> "EXCEPTION\n"+ <> " WHEN SQLSTATE 'XX000' THEN null; -- 'tuple concurrently updated': another connection installed it first\n"+ <> "END; $$"+ where+ functionName = "notify_job_queued_" <> tableName+ insertTriggerName = "did_insert_job_" <> tableName+ updateTriggerName = "did_update_job_" <> tableName++-- | Retuns the event name of the event that the pg notify trigger dispatches+channelName :: ByteString -> ByteString+channelName tableName = "job_available_" <> tableName
@@ -1,198 +1,22 @@ {-# LANGUAGE AllowAmbiguousTypes #-}-{-|-Module: IHP.Job.Runner-Description: Functions to run jobs-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.Job.Runner where--import IHP.Prelude-import IHP.ControllerPrelude-import IHP.ScriptSupport-import qualified IHP.Job.Queue as Queue-import qualified Control.Exception.Safe as Exception-import qualified Database.PostgreSQL.Simple.FromField as PG-import qualified Data.UUID.V4 as UUID-import qualified Control.Concurrent as Concurrent-import qualified Control.Concurrent.Async as Async-import qualified System.Posix.Signals as Signals-import qualified System.Exit as Exit-import qualified System.Timeout as Timeout-import qualified IHP.PGListener as PGListener-import Control.Monad.Trans.Resource-import qualified IHP.Log as Log---- | Used by the RunJobs binary-runJobWorkers :: [JobWorker] -> Script-runJobWorkers jobWorkers = dedicatedProcessMainLoop jobWorkers---- | This job worker main loop is used when the job workers are running as part of their own binary------ In dev mode the IHP dev server is using the 'devServerMainLoop' instead. We have two main loops--- as the stop handling works a different in those cases.----dedicatedProcessMainLoop :: (?modelContext :: ModelContext, ?context :: FrameworkConfig) => [JobWorker] -> IO ()-dedicatedProcessMainLoop jobWorkers = do- threadId <- Concurrent.myThreadId- exitSignalsCount <- newIORef 0- workerId <- UUID.nextRandom- let logger = ?context.logger-- Log.info ("Starting worker " <> tshow workerId)-- -- The job workers use their own dedicated PG listener as e.g. AutoRefresh or DataSync- -- could overload the main PGListener connection. In that case we still want jobs to be- -- run independent of the system being very busy.- PGListener.withPGListener ?modelContext \pgListener -> do- stopSignal <- Concurrent.newEmptyMVar-- runResourceT do- waitForExitSignal <- liftIO installSignalHandlers-- let jobWorkerArgs = JobWorkerArgs { workerId, modelContext = ?modelContext, frameworkConfig = ?context, pgListener }- - processes <- jobWorkers- |> mapM (\(JobWorker listenAndRun)-> listenAndRun jobWorkerArgs)-- liftIO waitForExitSignal-- liftIO $ Log.info ("Waiting for jobs to complete. CTRL+C again to force exit" :: Text)-- -- Stop subscriptions and poller already- -- This will stop all producers for the queue MVar- liftIO $ forEach processes \JobWorkerProcess { pollerReleaseKey, subscription, action } -> do- PGListener.unsubscribe subscription pgListener- release pollerReleaseKey- Concurrent.putMVar action Stop-- liftIO $ PGListener.stop pgListener-- -- While waiting for all jobs to complete, we also wait for another exit signal- -- If the user sends two exit signals, we just kill all processes- liftIO $ async do- waitForExitSignal-- Log.info ("Canceling all running jobs. CTRL+C again to force exit" :: Text)- - forEach processes \JobWorkerProcess { runners } -> do- forEach runners \(releaseKey, _) -> release releaseKey-- Concurrent.throwTo threadId Exit.ExitSuccess-- pure ()-- -- Wait for all runners to complete- liftIO $ forEach processes \JobWorkerProcess { runners } -> do- forEach runners \(_, async) -> Async.wait async-- liftIO $ Concurrent.throwTo threadId Exit.ExitSuccess--devServerMainLoop :: (?modelContext :: ModelContext) => FrameworkConfig -> PGListener.PGListener -> [JobWorker] -> IO ()-devServerMainLoop frameworkConfig pgListener jobWorkers = do- workerId <- UUID.nextRandom- let ?context = frameworkConfig- let logger = frameworkConfig.logger-- Log.info ("Starting worker " <> tshow workerId)- - runResourceT do- let jobWorkerArgs = JobWorkerArgs { workerId, modelContext = ?modelContext, frameworkConfig = ?context, pgListener }- - processes <- jobWorkers- |> mapM (\(JobWorker listenAndRun) -> listenAndRun jobWorkerArgs)-- liftIO $ (forever (Concurrent.threadDelay maxBound)) `Exception.finally` do- forEach processes \JobWorkerProcess { action } -> do- Concurrent.putMVar action Stop---- | Installs signals handlers and returns an IO action that blocks until the next sigINT or sigTERM is sent-installSignalHandlers :: IO (IO ())-installSignalHandlers = do- exitSignal <- Concurrent.newEmptyMVar-- let catchHandler = Concurrent.putMVar exitSignal ()- - Signals.installHandler Signals.sigINT (Signals.Catch catchHandler) Nothing- Signals.installHandler Signals.sigTERM (Signals.Catch catchHandler) Nothing-- pure (Concurrent.takeMVar exitSignal)--stopExitHandler JobWorkerArgs { .. } main = main--worker :: forall job.- ( job ~ GetModelByTableName (GetTableName job)- , FilterPrimaryKey (GetTableName job)- , FromRow job- , Show (PrimaryKey (GetTableName job))- , PG.FromField (PrimaryKey (GetTableName job))- , KnownSymbol (GetTableName job)- , SetField "attemptsCount" job Int- , SetField "lockedBy" job (Maybe UUID)- , SetField "status" job JobStatus- , SetField "updatedAt" job UTCTime- , HasField "runAt" job UTCTime- , HasField "attemptsCount" job Int- , SetField "lastError" job (Maybe Text)- , Job job- , CanUpdate job- , Show job- , Table job- ) => JobWorker-worker = JobWorker (jobWorkerFetchAndRunLoop @job)---jobWorkerFetchAndRunLoop :: forall job.- ( job ~ GetModelByTableName (GetTableName job)- , FilterPrimaryKey (GetTableName job)- , FromRow job- , Show (PrimaryKey (GetTableName job))- , PG.FromField (PrimaryKey (GetTableName job))- , KnownSymbol (GetTableName job)- , SetField "attemptsCount" job Int- , SetField "lockedBy" job (Maybe UUID)- , SetField "status" job JobStatus- , SetField "updatedAt" job UTCTime- , HasField "attemptsCount" job Int- , SetField "lastError" job (Maybe Text)- , Job job- , CanUpdate job- , Show job- , Table job- ) => JobWorkerArgs -> ResourceT IO JobWorkerProcess-jobWorkerFetchAndRunLoop JobWorkerArgs { .. } = do- let ?context = frameworkConfig- let ?modelContext = modelContext- action <- liftIO $ Concurrent.newMVar JobAvailable- let loop = do- receivedAction <- Concurrent.takeMVar action-- case receivedAction of- JobAvailable -> do- maybeJob <- Queue.fetchNextJob @job (timeoutInMicroseconds @job) (backoffStrategy @job) workerId- case maybeJob of- Just job -> do- Log.info ("Starting job: " <> tshow job)-- let ?job = job- let timeout :: Int = fromMaybe (-1) (timeoutInMicroseconds @job)- resultOrException <- Exception.tryAsync (Timeout.timeout timeout (perform job))- case resultOrException of- Left exception -> do- Queue.jobDidFail job exception- when (Exception.isAsyncException exception) (Exception.throwIO exception)- Right Nothing -> Queue.jobDidTimeout job- Right (Just _) -> Queue.jobDidSucceed job-- loop- Nothing -> loop- Stop -> do- -- Put the stop signal back in to stop the other runners as well- Concurrent.putMVar action Stop- pure ()-- runners <- forM [1..(maxConcurrency @job)] \index -> allocate (async loop) cancel-- (subscription, pollerReleaseKey) <- Queue.watchForJob pgListener (tableName @job) (queuePollInterval @job) (timeoutInMicroseconds @job) (backoffStrategy @job) action-+module IHP.Job.Runner+( runJobWorkers+, dedicatedProcessMainLoop+, devServerMainLoop+, installSignalHandlers+, stopExitHandler+, worker+, jobWorkerFetchAndRunLoop+) where - pure JobWorkerProcess { runners, subscription, pollerReleaseKey, action }+import IHP.Job.Runner.MainLoop+ ( runJobWorkers+ , dedicatedProcessMainLoop+ , devServerMainLoop+ , installSignalHandlers+ , stopExitHandler+ )+import IHP.Job.Runner.WorkerLoop+ ( worker+ , jobWorkerFetchAndRunLoop+ )
@@ -0,0 +1,123 @@+module IHP.Job.Runner.MainLoop+( runJobWorkers+, dedicatedProcessMainLoop+, devServerMainLoop+, installSignalHandlers+, stopExitHandler+) where++import IHP.Prelude+import IHP.ControllerPrelude+import IHP.ScriptSupport+import qualified Data.UUID.V4 as UUID+import qualified Control.Concurrent as Concurrent+import qualified Control.Concurrent.Async as Async+import qualified System.Posix.Signals as Signals+import qualified System.Exit as Exit+import qualified IHP.PGListener as PGListener+import Control.Monad.Trans.Resource+import qualified Control.Exception.Safe as Exception+import qualified IHP.Log as Log+import Control.Concurrent.STM (atomically, writeTBQueue)++-- | Used by the RunJobs binary+runJobWorkers :: [JobWorker] -> Script+runJobWorkers jobWorkers = dedicatedProcessMainLoop jobWorkers++-- | This job worker main loop is used when the job workers are running as part of their own binary+--+-- In dev mode the IHP dev server is using the 'devServerMainLoop' instead. We have two main loops+-- as the stop handling works a different in those cases.+--+dedicatedProcessMainLoop :: (?modelContext :: ModelContext, ?context :: FrameworkConfig) => [JobWorker] -> IO ()+dedicatedProcessMainLoop jobWorkers = do+ threadId <- Concurrent.myThreadId+ exitSignalsCount <- newIORef 0+ workerId <- UUID.nextRandom+ let logger = ?context.logger++ Log.info ("Starting worker " <> tshow workerId)++ -- The job workers use their own dedicated PG listener as e.g. AutoRefresh or DataSync+ -- could overload the main PGListener connection. In that case we still want jobs to be+ -- run independent of the system being very busy.+ PGListener.withPGListener ?context.databaseUrl ?context.logger \pgListener -> do+ stopSignal <- Concurrent.newEmptyMVar++ runResourceT do+ waitForExitSignal <- liftIO installSignalHandlers++ let jobWorkerArgs = JobWorkerArgs { workerId, modelContext = ?modelContext, frameworkConfig = ?context, pgListener }++ processes <- jobWorkers+ |> mapM (\(JobWorker listenAndRun)-> listenAndRun jobWorkerArgs)++ liftIO waitForExitSignal++ liftIO $ Log.info ("Waiting for jobs to complete. CTRL+C again to force exit" :: Text)++ -- Stop subscriptions and poller already+ -- This will stop all producers for the queue+ liftIO $ forEach processes \JobWorkerProcess { pollerReleaseKey, subscription, action, staleRecoveryReleaseKey } -> do+ PGListener.unsubscribe subscription pgListener+ release pollerReleaseKey+ case staleRecoveryReleaseKey of+ Just key -> release key+ Nothing -> pure ()+ -- Single Stop for the dispatcher (it waits for active workers internally)+ atomically $ writeTBQueue action Stop++ liftIO $ PGListener.stop pgListener++ -- While waiting for all jobs to complete, we also wait for another exit signal+ -- If the user sends two exit signals, we just kill all processes+ liftIO $ async do+ waitForExitSignal++ Log.info ("Canceling all running jobs. CTRL+C again to force exit" :: Text)++ forEach processes \JobWorkerProcess { dispatcher = (dispatcherKey, _) } -> do+ release dispatcherKey -- cancels dispatcher, whose finally cancels all workers++ Concurrent.throwTo threadId Exit.ExitSuccess++ pure ()++ -- Wait for dispatchers (which wait for their workers before exiting)+ liftIO $ forEach processes \JobWorkerProcess { dispatcher = (_, dispatcherAsync) } -> do+ Async.wait dispatcherAsync++ liftIO $ Concurrent.throwTo threadId Exit.ExitSuccess++devServerMainLoop :: (?modelContext :: ModelContext) => FrameworkConfig -> PGListener.PGListener -> [JobWorker] -> IO ()+devServerMainLoop frameworkConfig pgListener jobWorkers = do+ workerId <- UUID.nextRandom+ let ?context = frameworkConfig+ let logger = frameworkConfig.logger++ Log.info ("Starting worker " <> tshow workerId)++ runResourceT do+ let jobWorkerArgs = JobWorkerArgs { workerId, modelContext = ?modelContext, frameworkConfig = ?context, pgListener }++ processes <- jobWorkers+ |> mapM (\(JobWorker listenAndRun) -> listenAndRun jobWorkerArgs)++ liftIO $ (forever (Concurrent.threadDelay maxBound)) `Exception.finally` do+ forEach processes \JobWorkerProcess { action } -> do+ atomically $ writeTBQueue action Stop++-- | Installs signals handlers and returns an IO action that blocks until the next sigINT or sigTERM is sent+installSignalHandlers :: IO (IO ())+installSignalHandlers = do+ exitSignal <- Concurrent.newEmptyMVar++ let catchHandler = Concurrent.putMVar exitSignal ()++ Signals.installHandler Signals.sigINT (Signals.Catch catchHandler) Nothing+ Signals.installHandler Signals.sigTERM (Signals.Catch catchHandler) Nothing++ pure (Concurrent.takeMVar exitSignal)++stopExitHandler :: JobWorkerArgs -> IO a -> IO a+stopExitHandler JobWorkerArgs { .. } main = main
@@ -0,0 +1,139 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+module IHP.Job.Runner.WorkerLoop+( worker+, jobWorkerFetchAndRunLoop+) where++import IHP.Prelude+import IHP.ControllerPrelude+import qualified IHP.Environment as Environment+import qualified IHP.Job.Queue as Queue+import qualified Control.Exception.Safe as Exception+import qualified Control.Concurrent as Concurrent+import qualified Control.Concurrent.Async as Async+import qualified System.Timeout as Timeout+import Control.Monad.Trans.Resource+import qualified IHP.Log as Log+import IHP.Hasql.FromRow (FromRowHasql)+import Control.Concurrent.STM (atomically, newTBQueue, readTBQueue, writeTBQueue, newTVarIO, readTVar, readTVarIO, writeTVar, modifyTVar', check)+import IHP.Job.Queue (tryWriteTBQueue)++worker :: forall job.+ ( job ~ GetModelByTableName (GetTableName job)+ , FromRowHasql job+ , Show (PrimaryKey (GetTableName job))+ , KnownSymbol (GetTableName job)+ , HasField "id" job (Id' (GetTableName job))+ , PrimaryKey (GetTableName job) ~ UUID+ , HasField "runAt" job UTCTime+ , HasField "attemptsCount" job Int+ , Job job+ , Show job+ , Table job+ ) => JobWorker+worker = JobWorker (jobWorkerFetchAndRunLoop @job)+++jobWorkerFetchAndRunLoop :: forall job.+ ( job ~ GetModelByTableName (GetTableName job)+ , FromRowHasql job+ , Show (PrimaryKey (GetTableName job))+ , KnownSymbol (GetTableName job)+ , HasField "id" job (Id' (GetTableName job))+ , PrimaryKey (GetTableName job) ~ UUID+ , HasField "runAt" job UTCTime+ , HasField "attemptsCount" job Int+ , Job job+ , Show job+ , Table job+ ) => JobWorkerArgs -> ResourceT IO JobWorkerProcess+jobWorkerFetchAndRunLoop JobWorkerArgs { .. } = do+ let ?context = frameworkConfig+ let ?modelContext = modelContext+ let pool = modelContext.hasqlPool+ action <- liftIO $ atomically $ newTBQueue (fromIntegral (maxConcurrency @job))+ -- Seed the queue with one initial JobAvailable so the dispatcher attempts a fetch on startup+ liftIO $ atomically $ writeTBQueue action JobAvailable++ activeCount <- liftIO $ newTVarIO (0 :: Int)+ activeWorkers <- liftIO $ newTVarIO ([] :: [Async ()])++ let runJobLoop = do+ fetchResult <- Exception.tryAny (Queue.fetchNextJob @job pool workerId)+ case fetchResult of+ Left exception -> do+ Log.error ("Job worker: Failed to fetch next job: " <> tshow exception)+ Concurrent.threadDelay 1000000 -- 1s backoff to avoid tight error loops+ runJobLoop -- retry after transient error+ Right (Just job) -> do+ Log.info ("Starting job: " <> tshow job)++ let ?job = job+ let timeout :: Int = fromMaybe (-1) (timeoutInMicroseconds @job)+ resultOrException <- Exception.tryAsync (Timeout.timeout timeout (perform job))+ case resultOrException of+ Left exception -> do+ Queue.jobDidFail pool job exception+ when (Exception.isAsyncException exception) (Exception.throwIO exception)+ Right Nothing -> Queue.jobDidTimeout pool job+ Right (Just _) -> Queue.jobDidSucceed pool job++ runJobLoop -- try next job immediately+ Right Nothing -> pure ()++ let dispatcherLoop = do+ msg <- atomically $ readTBQueue action+ case msg of+ Stop -> do+ -- Wait for all active workers to finish+ atomically $ do+ count <- readTVar activeCount+ check (count == 0)+ JobAvailable -> do+ acquired <- atomically $ do+ count <- readTVar activeCount+ if count < maxConcurrency @job+ then do+ writeTVar activeCount (count + 1)+ pure True+ else pure False+ when acquired do+ selfVar <- Concurrent.newEmptyMVar+ workerAsync <- async $+ (do self <- Concurrent.readMVar selfVar+ runJobLoop)+ `Exception.finally`+ (do maybeSelf <- Concurrent.tryReadMVar selfVar+ atomically do+ modifyTVar' activeCount (subtract 1)+ case maybeSelf of+ Just self -> modifyTVar' activeWorkers (filter (/= self))+ Nothing -> pure ())+ Concurrent.putMVar selfVar workerAsync+ atomically $ modifyTVar' activeWorkers (workerAsync :)+ dispatcherLoop++ let cancelAllWorkers = do+ workers <- readTVarIO activeWorkers+ mapM_ Async.cancel workers++ dispatcher <- allocate (async (dispatcherLoop `Exception.finally` cancelAllWorkers)) cancel++ let enablePollerTriggerRepair = frameworkConfig.environment == Environment.Development+ (subscription, pollerReleaseKey) <- Queue.watchForJobWithPollerTriggerRepair enablePollerTriggerRepair pool pgListener (tableName @job) (queuePollInterval @job) action++ -- Start stale job recovery if configured+ staleRecoveryReleaseKey <- case staleJobTimeout @job of+ Just threshold -> do+ let intervalMicroseconds = round (threshold / 2) * 1000000+ let recoveryLoop = forever do+ Queue.recoverStaleJobs @job pool threshold+ -- Signal workers to check for recovered jobs+ _ <- atomically $ tryWriteTBQueue action JobAvailable+ pure ()+ Concurrent.threadDelay intervalMicroseconds+ (key, _) <- allocate (Async.async recoveryLoop) Async.cancel+ pure (Just key)+ Nothing -> pure Nothing++ pure JobWorkerProcess { dispatcher, subscription, pollerReleaseKey, action, staleRecoveryReleaseKey, activeCount }
@@ -11,75 +11,7 @@ ) where -import IHP.Prelude-import IHP.FrameworkConfig-import qualified IHP.PGListener as PGListener-import qualified Control.Concurrent as Concurrent-import Control.Monad.Trans.Resource--data BackoffStrategy- = LinearBackoff { delayInSeconds :: !Int }- | ExponentialBackoff { delayInSeconds :: !Int }- deriving (Eq, Show)--class Job job where- perform :: (?modelContext :: ModelContext, ?context :: FrameworkConfig) => job -> IO ()-- maxAttempts :: (?job :: job) => Int- maxAttempts = 10-- timeoutInMicroseconds :: Maybe Int- timeoutInMicroseconds = Nothing-- -- | While jobs are typically fetch using pg_notiy, we have to poll the queue table- -- periodically to catch jobs with a @run_at@ in the future- --- -- By default we only poll every minute- queuePollInterval :: Int- queuePollInterval = 60 * 1000000-- -- | How many jobs of this type can be executed at the same time- --- -- This limit only applies to the running haskell process. If you run @N@ multiple- -- independent processes of the job runner, the limit will be @N * maxConcurrency@- maxConcurrency :: Int- maxConcurrency = 16-- backoffStrategy :: BackoffStrategy- backoffStrategy = LinearBackoff { delayInSeconds = 30 }--class Worker application where- workers :: application -> [JobWorker]--data JobWorkerArgs = JobWorkerArgs- { workerId :: UUID- , modelContext :: ModelContext- , frameworkConfig :: FrameworkConfig- , pgListener :: PGListener.PGListener- }--newtype JobWorker = JobWorker (JobWorkerArgs -> ResourceT IO JobWorkerProcess)---- | Mapping for @JOB_STATUS@. The DDL statement for this can be found in IHPSchema.sql:------ > CREATE TYPE JOB_STATUS AS ENUM ('job_status_not_started', 'job_status_running', 'job_status_failed', 'job_status_succeeded', 'job_status_retry');-data JobStatus- = JobStatusNotStarted- | JobStatusRunning- | JobStatusFailed- | JobStatusTimedOut- | JobStatusSucceeded- | JobStatusRetry- deriving (Eq, Show, Read, Enum)--data JobWorkerProcess- = JobWorkerProcess- { runners :: [(ReleaseKey, Async ())]- , subscription :: PGListener.Subscription- , pollerReleaseKey :: ReleaseKey- , action :: Concurrent.MVar JobWorkerProcessMessage- }--data JobWorkerProcessMessage- = JobAvailable- | Stop+import IHP.Job.Types.BackoffStrategy+import IHP.Job.Types.Class+import IHP.Job.Types.Status+import IHP.Job.Types.Worker
@@ -0,0 +1,10 @@+module IHP.Job.Types.BackoffStrategy+( BackoffStrategy (..)+) where++import IHP.Prelude++data BackoffStrategy+ = LinearBackoff { delayInSeconds :: !Int }+ | ExponentialBackoff { delayInSeconds :: !Int }+ deriving (Eq, Show)
@@ -0,0 +1,41 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+module IHP.Job.Types.Class+( Job (..)+) where++import IHP.Prelude+import IHP.FrameworkConfig.Types (FrameworkConfig)+import IHP.Job.Types.BackoffStrategy (BackoffStrategy (..))++class Job job where+ perform :: (?modelContext :: ModelContext, ?context :: FrameworkConfig) => job -> IO ()++ maxAttempts :: (?job :: job) => Int+ maxAttempts = 10++ timeoutInMicroseconds :: Maybe Int+ timeoutInMicroseconds = Nothing++ -- | While jobs are typically fetch using pg_notiy, we have to poll the queue table+ -- periodically to catch jobs with a @run_at@ in the future+ --+ -- By default we only poll every minute+ queuePollInterval :: Int+ queuePollInterval = 60 * 1000000++ -- | How many jobs of this type can be executed at the same time+ --+ -- This limit only applies to the running haskell process. If you run @N@ multiple+ -- independent processes of the job runner, the limit will be @N * maxConcurrency@+ maxConcurrency :: Int+ maxConcurrency = 16++ backoffStrategy :: BackoffStrategy+ backoffStrategy = LinearBackoff { delayInSeconds = 30 }++ -- | How long a job can be in 'JobStatusRunning' before it's considered stale+ -- and recovered. Set to 'Nothing' to disable stale job recovery.+ --+ -- Default: 10 minutes+ staleJobTimeout :: Maybe NominalDiffTime+ staleJobTimeout = Just 600
@@ -0,0 +1,17 @@+module IHP.Job.Types.Status+( JobStatus (..)+) where++import IHP.Prelude++-- | Mapping for @JOB_STATUS@. The DDL statement for this can be found in IHPSchema.sql:+--+-- > CREATE TYPE JOB_STATUS AS ENUM ('job_status_not_started', 'job_status_running', 'job_status_failed', 'job_status_succeeded', 'job_status_retry');+data JobStatus+ = JobStatusNotStarted+ | JobStatusRunning+ | JobStatusFailed+ | JobStatusTimedOut+ | JobStatusSucceeded+ | JobStatusRetry+ deriving (Eq, Show, Read, Enum)
@@ -0,0 +1,39 @@+module IHP.Job.Types.Worker+( Worker (..)+, JobWorkerArgs (..)+, JobWorker (..)+, JobWorkerProcess (..)+, JobWorkerProcessMessage (..)+) where++import IHP.Prelude+import IHP.FrameworkConfig.Types (FrameworkConfig)+import qualified IHP.PGListener as PGListener+import Control.Monad.Trans.Resource (ResourceT, ReleaseKey)+import Control.Concurrent.STM (TBQueue, TVar)++class Worker application where+ workers :: application -> [JobWorker]++data JobWorkerArgs = JobWorkerArgs+ { workerId :: UUID+ , modelContext :: ModelContext+ , frameworkConfig :: FrameworkConfig+ , pgListener :: PGListener.PGListener+ }++newtype JobWorker = JobWorker (JobWorkerArgs -> ResourceT IO JobWorkerProcess)++data JobWorkerProcess+ = JobWorkerProcess+ { dispatcher :: (ReleaseKey, Async ())+ , subscription :: PGListener.Subscription+ , pollerReleaseKey :: ReleaseKey+ , action :: TBQueue JobWorkerProcessMessage+ , staleRecoveryReleaseKey :: Maybe ReleaseKey+ , activeCount :: TVar Int+ }++data JobWorkerProcessMessage+ = JobAvailable+ | Stop
@@ -1,122 +0,0 @@-{-|-Module: IHP.Log-Description: Functions to write logs at all log levels.--Import this module qualified! All code examples-assume you have imported the module as follows:--> import qualified IHP.Log as Log---}-module IHP.Log-( module IHP.Log.Types-, debug-, info-, warn-, error-, fatal-, unknown-, makeRequestLogger-, defaultRequestLogger-) where--import IHP.HaskellSupport hiding (debug)--import CorePrelude hiding (putStr, putStrLn, print, error, show, log, debug)-import Control.Monad (when)-import IHP.Log.Types-import Network.Wai (Middleware)-import Network.Wai.Middleware.RequestLogger (mkRequestLogger, RequestLoggerSettings, destination)-import qualified Network.Wai.Middleware.RequestLogger as RequestLogger-import Data.Default (Default (def))-import qualified System.Log.FastLogger as FastLogger---- | Format a log and send it to the logger.--- Internal use only -- application code should call the--- function corresponding to the desired log level.-log :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => LogLevel -> string -> IO ()-log level text = do- writeLog level ?context.logger text---- | Log a debug level message.------ > action CreateUserAction { .. } = do--- > Log.debug "entered CreateUserAction"--- > ...-debug :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()-debug = log Debug---- | Log an info level message.------ > action UsersAction = do--- > users <- query @User |> fetch--- > Log.info $ show (lengh users) <> " users fetched."--- > ...-info :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()-info = log Info---- | Log a warning level message.------ > action UsersAction = do--- > users <- query @User |> fetch--- > whenEmpty users $ Log.warn "No users found. Something might be wrong!"--- > ...-warn :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()-warn = log Warn---- |Log a warning level message.------ @--- action CreatePostAction = do--- let post = newRecord @Post--- post--- |> buildPost--- |> ifValid \case--- Left post -> do--- Log.error "Invalid post."--- render NewView { .. }--- Right post -> do--- post <- post |> createRecord--- setSuccessMessage "Post created"--- redirectTo PostsAction--- @-error :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()-error = log Error---- | Log a fatal level message.--- Note this does not exit the program for you -- it only logs to the "Fatal" log level.------ > Log.fatal "Unrecoverable application error!"-fatal :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()-fatal = log Fatal---- | Log an "unknown" level message.--- This is the highest log level and will always be output by the logger.------ > Log.unknown "This will be sent to the logger no matter what!"-unknown :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr string) => string -> IO ()-unknown = log Unknown---- | Write a log if the given log level is greater than or equal to the logger's log level.-writeLog :: (FastLogger.ToLogStr string) => LogLevel -> Logger -> string -> IO ()-writeLog level logger text = do- let write = logger.write- let formatter = logger.formatter- when (level >= logger.level) do- write (\time -> formatter time level (toLogStr text))---- | Wraps 'RequestLogger' from wai-extra to log to an IHP logger.--- See 'Network.Wai.Middleware.RequestLogger'.-makeRequestLogger :: RequestLoggerSettings -> Logger -> IO Middleware-makeRequestLogger settings logger = - mkRequestLogger settings {- destination = RequestLogger.Callback (\logStr ->- let ?context = logger in- logStr |> fromLogStr |> info- )- }---- | Create a request logger with default settings wrapped in an IHP logger.--- See 'Network.Wai.Middleware.RequestLogger'.-defaultRequestLogger :: Logger -> IO Middleware-defaultRequestLogger = makeRequestLogger def
@@ -1,252 +0,0 @@-{-# LANGUAGE ConstraintKinds #-}-{-|-Module: IHP.Log.Types-Description: Types for the IHP logging system--}--module IHP.Log.Types-( Bytes(..)-, LogStr-, BufSize-, TimeFormat-, RotateSettings(..)-, toLogStr-, fromLogStr-, defaultBufSize-, simpleTimeFormat-, simpleTimeFormat'-, Logger(..)-, LogLevel(..)-, LogDestination(..)-, LoggingProvider(..)-, LoggerSettings(..)-, LogFormatter-, FormattedTime-, newLogger-, defaultLogger-, defaultDestination-, defaultFormatter-, withLevelFormatter-, withTimeFormatter-, withTimeAndLevelFormatter-) where--import qualified Prelude-import CorePrelude hiding (putStr, putStrLn, print, error, show)-import Data.Text as Text-import Data.Default (Default (def))-import System.Log.FastLogger (- LogStr,- LogType'(..),- BufSize,- FileLogSpec(..),- TimedFileLogSpec(..),- TimeFormat,- toLogStr,- fromLogStr,- defaultBufSize,- newTimeCache,- simpleTimeFormat,- simpleTimeFormat',- newTimedFastLogger,- ToLogStr (..)- )--import qualified System.Log.FastLogger as FastLogger (FormattedTime)-import GHC.Records----- some functions brought over from IHP.Prelude--- can't import due to circular dependency with IHP.ModelSupport which relies on this module--tshow :: Show a => a -> Text-tshow value = Text.pack (Prelude.show value)--show :: Show a => a -> Text-show = tshow---- | Interal logger type that encapsulates information needed to perform--- logging operations. Users can also access this though the 'LoggingProvider'--- class in controller and model actions to perform logic based on the set log level.-data Logger = Logger {- write :: !((FastLogger.FormattedTime -> LogStr) -> IO ()),- level :: !LogLevel,- formatter :: !LogFormatter,- timeCache :: !(IO FastLogger.FormattedTime),- cleanup :: !(IO ())-}--data LogLevel- -- | For general messages to help with debugging during development.- -- Default log level in development.- -- Also the log level used for SQL queries.- -- See 'IHP.Log.debug' for example usage.- = Debug- -- | For info messages that help montior application usage.- -- Default log level for production.- -- See 'IHP.Log.info' for example usage.- | Info- -- | For warning messages when something might be wrong.- -- See 'IHP.Log.warn' for example usage.- | Warn- -- | For application errors that can be recovered from.- -- See 'IHP.Log.error' for example usage.- | Error- -- | For application errors that are fatal- -- See 'IHP.Log.fatal' for example usage.- | Fatal- -- | For miscallenaous log messages. Highest log level - will always be logged- -- See 'IHP.Log.unknown' for example usage.- | Unknown- deriving (Enum, Eq, Ord, Show)--instance ToLogStr LogLevel where- toLogStr Debug = "DEBUG"- toLogStr Info = "INFO"- toLogStr Warn = "WARN"- toLogStr Error = "ERROR"- toLogStr Fatal = "FATAL"- toLogStr Unknown = "UNKNOWN"---- | The timestamp in the formatted defined by the logger's timeFormat string.-type FormattedTime = ByteString---- | Called every time a message is sent to the logger.--- Since this is just a function type, it's trivial to define custom formatters:------ @--- withTimeAndLevelFormatterUpcaseAndHappy :: LogFormatter--- withTimeAndLevelFormatterUpcaseAndHappy time level msg =--- "[" <> toUpper (show level) <> "]"--- <> "[" <> time <> "] "--- <> toUpper msg <> " :) \n"--- @-type LogFormatter = FormattedTime -> LogLevel -> LogStr -> LogStr---- | A number of bytes, used in 'RotateSettings'-newtype Bytes = Bytes Integer--data RotateSettings- -- | Log messages to a file which is never rotated.- --- -- @- -- newLogger def {- -- destination = File "Log/production.log" NoRotate defaultBufSize- -- }- -- @- = NoRotate- -- | Log messages to a file and rotate the file after it reaches the given size in bytes.- -- Third argument is the max number of rotated log files to keep around before overwriting the oldest one.- --- -- Example: log to a file rotated once it is 4MB, and keep 7 files before overwriting the first file.- --- -- @- -- newLogger def {- -- destination = File "Log/production.log" (SizeRotate (Bytes (4 * 1024 * 1024)) 7) defaultBufSize- -- }- -- @- | SizeRotate !Bytes !Int- -- | Log messages to a file rotated on a timed basis.- -- Expects a time format string as well as a function which compares two formatted time strings- -- which is used to determine if the file should be rotated.- -- Last argument is a function which is called on a log file once its rotated.- --- -- Example: rotate a file daily and compress the log file once rotated.- --- -- @- -- let- -- filePath = "Log/production.log"- -- formatString = "%FT%H%M%S"- -- timeCompare = (==) on C8.takeWhile (/=T))- -- compressFile fp = void . forkIO $- -- callProcess "tar" [ "--remove-files", "-caf", fp <> ".gz", fp ]- -- in- -- newLogger def {- -- destination = File- -- filePath- -- (TimedRotate formatString timeCompare compressFile)- -- defaultBufSize- -- }- -- @- | TimedRotate !TimeFormat (FastLogger.FormattedTime -> FastLogger.FormattedTime -> Bool) (FilePath -> IO ())---- | Where logged messages will be delivered to.-data LogDestination- = None- -- | Log messages to standard output.- | Stdout !BufSize- -- | Log messages to standard error.- | Stderr !BufSize- -- | Log message to a file. Rotate the log file with the behavior given by 'RotateSettings'.- | File !FilePath !RotateSettings !BufSize- -- | Send logged messages to a callback. Flush action called after every log.- | Callback !(LogStr -> IO ()) !(IO ())--data LoggerSettings = LoggerSettings {- level :: LogLevel,- formatter :: LogFormatter,- destination :: LogDestination,- timeFormat :: TimeFormat-}--instance Default LoggerSettings where- def = LoggerSettings {- level = Debug,- formatter = defaultFormatter,- destination = defaultDestination,- timeFormat = simpleTimeFormat'- }---- | Logger default destination is to standard out.-defaultDestination :: LogDestination-defaultDestination = Stdout defaultBufSize---- | Used to get the logger for a given environment.--- | Call in any instance of 'LoggingProvider' get the the environment's current logger.--- Useful in controller and model actions, which both have logging contexts.-type LoggingProvider context = HasField "logger" context Logger--instance HasField "logger" Logger Logger where- getField logger = logger---- | Create a new 'FastLogger' and wrap it in an IHP 'Logger'.--- Use with the default logger settings and record update syntax for nice configuration:------ > newLogger def { level = Error }-newLogger :: LoggerSettings -> IO Logger-newLogger LoggerSettings { .. } = do- timeCache <- newTimeCache timeFormat- (write, cleanup) <- makeFastLogger timeCache destination- pure Logger { .. }- where- makeFastLogger timeCache destination = newTimedFastLogger timeCache $- case destination of- None -> LogNone- Stdout buf -> LogStdout buf- Stderr buf -> LogStderr buf- File path settings buf -> makeFileLogger path settings buf- Callback callback flush -> LogCallback callback flush-- makeFileLogger path NoRotate = LogFileNoRotate path- makeFileLogger path (SizeRotate (Bytes size) count) = LogFile (FileLogSpec path size count)- makeFileLogger path (TimedRotate fmt cmp post) = LogFileTimedRotate (TimedFileLogSpec path fmt cmp post)---- | Formats logs as-is to stdout.-defaultLogger :: IO Logger-defaultLogger = newLogger def---- | Formats the log as-is with a newline added.-defaultFormatter :: LogFormatter-defaultFormatter _ _ msg = msg <> "\n"---- | Prepends the timestamp to the log message and adds a new line.-withTimeFormatter :: LogFormatter-withTimeFormatter time _ msg = "[" <> toLogStr time <> "] " <> msg <> "\n"---- | Prepends the log level to the log message and adds a new line.-withLevelFormatter :: LogFormatter-withLevelFormatter time level msg = "[" <> (toLogStr level) <> "] " <> msg <> "\n"---- | Prepends the log level and timestamp to the log message and adds a new line.-withTimeAndLevelFormatter :: LogFormatter-withTimeAndLevelFormatter time level msg = "[" <> (toLogStr level) <> "] [" <> toLogStr time <> "] " <> msg <> "\n"
@@ -28,15 +28,14 @@ import IHP.Controller.Session import IHP.LoginSupport.Types import qualified IHP.Controller.Session as Session-import IHP.FlashMessages.ControllerFunctions+import IHP.FlashMessages import qualified IHP.ModelSupport as ModelSupport import IHP.ControllerSupport+import IHP.RequestVault.Helper (lookupRequestVault) import System.IO.Unsafe (unsafePerformIO) import IHP.AuthSupport.Authentication import IHP.Controller.Context import qualified IHP.FrameworkConfig as FrameworkConfig-import qualified Database.PostgreSQL.Simple.ToField as PG-import Data.Kind import Data.Typeable currentRoleOrNothing :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user) => Maybe user@@ -45,22 +44,22 @@ Nothing -> error ("initAuthentication @" <> show (typeRep (Proxy @user)) <> " has not been called in initContext inside FrontController of this application") {-# INLINE currentRoleOrNothing #-} -currentRole :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user) => user+currentRole :: forall user. (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl user, Typeable user) => user currentRole = fromMaybe (redirectToLogin (newSessionUrl (Proxy @user))) (currentRoleOrNothing @user) {-# INLINE currentRole #-} -currentRoleId :: forall user userId. (?context :: ControllerContext, HasNewSessionUrl user, HasField "id" user userId, Typeable user) => userId+currentRoleId :: forall user userId. (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl user, HasField "id" user userId, Typeable user) => userId currentRoleId = (currentRole @user).id {-# INLINE currentRoleId #-} -ensureIsRole :: forall (user :: Type). (?context :: ControllerContext, HasNewSessionUrl user, Typeable user) => IO ()+ensureIsRole :: forall (user :: Type). (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl user, Typeable user) => IO () ensureIsRole = case currentRoleOrNothing @user of Just _ -> pure () Nothing -> redirectToLoginWithMessage (newSessionUrl (Proxy :: Proxy user)) {-# INLINABLE ensureIsRole #-} -currentUser :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user, user ~ CurrentUserRecord) => user+currentUser :: forall user. (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl user, Typeable user, user ~ CurrentUserRecord) => user currentUser = currentRole @user {-# INLINABLE currentUser #-} @@ -68,15 +67,15 @@ currentUserOrNothing = currentRoleOrNothing @user {-# INLINABLE currentUserOrNothing #-} -currentUserId :: forall user userId. (?context :: ControllerContext, HasNewSessionUrl user, HasField "id" user userId, Typeable user, user ~ CurrentUserRecord) => userId+currentUserId :: forall user userId. (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl user, HasField "id" user userId, Typeable user, user ~ CurrentUserRecord) => userId currentUserId = currentRoleId @user {-# INLINABLE currentUserId #-} -ensureIsUser :: forall user. (?context :: ControllerContext, HasNewSessionUrl user, Typeable user, user ~ CurrentUserRecord) => IO ()+ensureIsUser :: forall user. (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl user, Typeable user, user ~ CurrentUserRecord) => IO () ensureIsUser = ensureIsRole @user {-# INLINABLE ensureIsUser #-} -currentAdmin :: forall admin. (?context :: ControllerContext, HasNewSessionUrl admin, Typeable admin, admin ~ CurrentAdminRecord) => admin+currentAdmin :: forall admin. (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl admin, Typeable admin, admin ~ CurrentAdminRecord) => admin currentAdmin = currentRole @admin {-# INLINABLE currentAdmin #-} @@ -84,11 +83,11 @@ currentAdminOrNothing = currentRoleOrNothing @admin {-# INLINABLE currentAdminOrNothing #-} -currentAdminId :: forall admin adminId. (?context :: ControllerContext, HasNewSessionUrl admin, HasField "id" admin adminId, Typeable admin, admin ~ CurrentAdminRecord) => adminId+currentAdminId :: forall admin adminId. (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl admin, HasField "id" admin adminId, Typeable admin, admin ~ CurrentAdminRecord) => adminId currentAdminId = currentRoleId @admin {-# INLINABLE currentAdminId #-} -ensureIsAdmin :: forall (admin :: Type). (?context :: ControllerContext, HasNewSessionUrl admin, Typeable admin, admin ~ CurrentAdminRecord) => IO ()+ensureIsAdmin :: forall (admin :: Type). (?context :: ControllerContext, ?request :: Request, HasNewSessionUrl admin, Typeable admin, admin ~ CurrentAdminRecord) => IO () ensureIsAdmin = ensureIsRole @admin {-# INLINABLE ensureIsAdmin #-} @@ -102,7 +101,7 @@ -- > -- > redirectToPath "/" ---login :: forall user id. (?context :: ControllerContext, KnownSymbol (ModelSupport.GetModelName user), HasField "id" user id, Show id) => user -> IO ()+login :: forall user id. (?request :: Request, KnownSymbol (ModelSupport.GetModelName user), HasField "id" user id, Show id) => user -> IO () login user = Session.setSession (sessionKey @user) (tshow (user.id)) {-# INLINABLE login #-} @@ -113,10 +112,10 @@ -- > action LogoutAction = do -- > let user = currentUser -- > logout user--- > +-- > -- > redirectToPath "/" ---logout :: forall user. (?context :: ControllerContext, KnownSymbol (ModelSupport.GetModelName user)) => user -> IO ()+logout :: forall user. (?request :: Request, KnownSymbol (ModelSupport.GetModelName user)) => user -> IO () logout user = Session.setSession (sessionKey @user) ("" :: Text) {-# INLINABLE logout #-} @@ -124,7 +123,7 @@ sessionKey = "login." <> cs (ModelSupport.getModelName @user) {-# INLINABLE sessionKey #-} -redirectToLoginWithMessage :: (?context :: ControllerContext) => Text -> IO ()+redirectToLoginWithMessage :: (?request :: Request) => Text -> IO () redirectToLoginWithMessage newSessionPath = do setSuccessMessage "Please log in to access this page" setSession "IHP.LoginSupport.redirectAfterLogin" getRequestPathAndQuery@@ -132,7 +131,7 @@ error "Unreachable" -redirectToLogin :: (?context :: ControllerContext) => Text -> a+redirectToLogin :: (?request :: Request) => Text -> a redirectToLogin newSessionPath = unsafePerformIO $ do redirectToPath newSessionPath error "Unreachable"@@ -159,16 +158,17 @@ -- enableRowLevelSecurityIfLoggedIn :: ( ?context :: ControllerContext+ , ?request :: Request , Typeable CurrentUserRecord , HasNewSessionUrl CurrentUserRecord , HasField "id" CurrentUserRecord userId- , PG.ToField userId+ , Show userId ) => IO () enableRowLevelSecurityIfLoggedIn = do case currentUserOrNothing of Just user -> do let rlsAuthenticatedRole = ?context.frameworkConfig.rlsAuthenticatedRole- let rlsUserId = PG.toField user.id+ let rlsUserId = tshow user.id let rlsContext = ModelSupport.RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId}- putContext rlsContext+ writeIORef (lookupRequestVault rlsContextVaultKey ?request) (Just rlsContext) Nothing -> pure ()
@@ -10,15 +10,17 @@ import IHP.ControllerSupport import IHP.ModelSupport import IHP.Controller.Context+import IHP.Hasql.FromRow (FromRowHasql) {-# INLINE initAuthentication #-} initAuthentication :: forall user normalizedModel. ( ?context :: ControllerContext+ , ?request :: Request , ?modelContext :: ModelContext , normalizedModel ~ NormalizeModel user , Typeable normalizedModel , Table normalizedModel- , FromRow normalizedModel+ , FromRowHasql normalizedModel , PrimaryKey (GetTableName normalizedModel) ~ UUID , GetTableName normalizedModel ~ GetTableName user , FilterPrimaryKey (GetTableName normalizedModel)
@@ -1,153 +0,0 @@-{-|-Module: IHP.Mail-Description: Send Emails-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.Mail-( MailServer (..)-, BuildMail (..)-, SMTPEncryption ( ..)-, sendMail-, sendWithMailServer-)-where--import IHP.Prelude-import IHP.Mail.Types-import IHP.FrameworkConfig--import Network.Mail.Mime-import qualified Network.Mail.Mime.SES as Mailer-import qualified Network.Mail.SMTP as SMTP-import qualified Network.HTTP.Client-import qualified Network.HTTP.Client.TLS-import Text.Blaze.Html5 (Html)-import qualified Text.Blaze.Html.Renderer.Text as Blaze-import qualified Data.Text as Text-import Data.Maybe--buildMail :: (BuildMail mail, ?context :: context, ConfigProvider context) => mail -> Mail-buildMail mail =- let ?mail = mail in- let mail' = simpleMailInMemory (to mail) from subject (cs $ text mail) (html mail |> Blaze.renderHtml) attachments' in- mail' { mailCc = cc mail- , mailBcc = bcc mail- , mailHeaders = ("Subject", subject) : h- }- where- h = case replyTo mail of- Nothing -> headers mail- Just replyTo -> ("Reply-To", renderAddress replyTo) : (headers mail)-- attachments' = mail- |> attachments- |> map (\MailAttachment { name, content, contentType } -> (contentType, name, content))---- | Sends an email------ Uses the mail server provided in the controller context, configured in Config/Config.hs-sendMail :: (BuildMail mail, ?context :: context, ConfigProvider context) => mail -> IO ()-sendMail mail = sendWithMailServer ?context.frameworkConfig.mailServer (buildMail mail)--sendWithMailServer :: MailServer -> Mail -> IO ()-sendWithMailServer SES { .. } mail = do- manager <- Network.HTTP.Client.newManager Network.HTTP.Client.TLS.tlsManagerSettings- let ses = Mailer.SES {- Mailer.sesFrom = cs $ addressEmail (mailFrom mail),- Mailer.sesTo = map (cs . addressEmail) ((mailTo mail) <> (mailCc mail) <> (mailBcc mail)),- Mailer.sesAccessKey = accessKey,- Mailer.sesSecretKey = secretKey,- Mailer.sesSessionToken = Nothing,- Mailer.sesRegion = region- }- Mailer.renderSendMailSES manager ses mail--sendWithMailServer SendGrid { .. } mail = do- let mail' = if isJust category then mail {mailHeaders = ("X-SMTPAPI","{\"category\": \"" ++ (fromJust category) ++ "\"}") : headers} else mail- SMTP.sendMailWithLoginSTARTTLS' "smtp.sendgrid.net" 587 "apikey" (Text.unpack apiKey) mail'- where headers = mailHeaders mail--sendWithMailServer IHP.Mail.Types.SMTP { .. } mail- | isNothing credentials =- case encryption of- Unencrypted -> SMTP.sendMail' host port mail- TLS -> SMTP.sendMailTLS' host port mail- STARTTLS -> SMTP.sendMailSTARTTLS' host port mail- | otherwise =- case encryption of- Unencrypted -> SMTP.sendMailWithLogin' host port (fst creds) (snd creds) mail- TLS -> SMTP.sendMailWithLoginTLS' host port (fst creds) (snd creds) mail- STARTTLS -> SMTP.sendMailWithLoginSTARTTLS' host port (fst creds) (snd creds) mail- where creds = fromJust credentials--sendWithMailServer Sendmail mail = do- message <- renderMail' mail- sendmail message--class BuildMail mail where- -- | You can use @?mail@ to make this dynamic based on the given entity- subject :: (?mail :: mail) => Text-- -- | The email receiver- --- -- __Example:__- --- -- > to ConfirmationMail { .. } = Address { addressName = Just (user.name), addressEmail = user.email }- --- -- __Example:__ Send all emails to a fixed email address while in development mode- --- -- > to CreateAccountMail { .. } = Address- -- > { addressName = Just (fullName admin)- -- > , addressEmail =- -- > if isDevelopment then- -- > "staging@example.com"- -- > else- -- > admin.email- -- > }- --- to :: (?context :: context, ConfigProvider context) => mail -> Address-- -- | Sets an optional reply-to address- replyTo :: (?context :: context, ConfigProvider context) => mail -> Maybe Address- replyTo mail = Nothing-- -- | Public list of addresses to receive a copy of the mail (CC)- cc :: (?context :: context, ConfigProvider context) => mail -> [Address]- cc mail = []-- -- | Hidden list of addresses to receive a copy of the mail (BCC)- bcc :: (?context :: context, ConfigProvider context) => mail -> [Address]- bcc mail = []-- -- | Custom headers, excluding @from@, @to@, @cc@, @bcc@, @subject@, and @reply-to@- --- -- __Example:__ Add a custom X-Mailer header- --- -- > headers CreateAccountMail { .. } = [("X-Mailer", "mail4j 2.17.0")]- --- headers :: (?context :: context, ConfigProvider context) => mail -> Headers- headers mail = []-- -- | Your sender address- --- -- __Example:__- --- -- > from = Address { addressName = "Acme Inc.", addressEmail = "hi@example.com" }- --- from :: (?mail :: mail, ?context :: context, ConfigProvider context) => Address-- -- | Similiar to a normal html view, HSX can be used here- html :: (?context :: context, ConfigProvider context) => mail -> Html-- -- | When no plain text version of the email is specified it falls back to using the html version but striping out all the html tags- text :: (?context :: context, ConfigProvider context) => mail -> Text- text mail = stripTags (cs $ Blaze.renderHtml (html mail))-- -- | Optional, mail attachments- --- -- __Example:__- --- -- > attachments = [ MailAttachment { name = "attached_file.xml", contentType = "application/xml", content = "<xml><hello/></xml>" } ]- --- attachments :: (?context :: context, ConfigProvider context) => mail -> [MailAttachment]- attachments mail = []
@@ -1,41 +0,0 @@-{-|-Module: IHP.Mail.Types-Description: Types for emails-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.Mail.Types-( MailServer (..)-, MailAttachment (..)-, SMTPEncryption (..)-)-where--import IHP.Prelude-import Network.Socket (PortNumber)---- | Configuration for a mailer used by IHP-data SMTPEncryption = Unencrypted | TLS | STARTTLS--data MailServer =- -- | Uses AWS SES for sending emails- SES { accessKey :: ByteString- , secretKey :: ByteString- -- | E.g. @"us-east-1"@ or @"eu-west-1"@- , region :: Text }- -- | Uses the local Sendmail binary for sending emails- | Sendmail- -- | Uses SendGrid for sending emails- | SendGrid { apiKey :: Text- , category :: Maybe Text }- -- | Uses a generic SMTP server for sending emails- | SMTP { host :: String- , port :: PortNumber- -- (Username,Password) combination- , credentials :: Maybe (String, String)- , encryption :: SMTPEncryption }--data MailAttachment = MailAttachment- { name :: Text -- ^ File name of an attachment- , content :: LByteString- , contentType :: Text- } deriving (Eq, Show)
@@ -1,16 +0,0 @@-{-|-Module: IHP.MailPrelude-Description: Prelude for email views-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.MailPrelude-( module IHP.Mail-, module Network.Mail.Mime-, module IHP.ViewPrelude-, module IHP.Mail.Types-) where--import IHP.Mail-import IHP.Mail.Types-import Network.Mail.Mime-import IHP.ViewPrelude hiding (html)
@@ -1,21 +0,0 @@-{-|-Module: IHP.Modal.ControllerFunctions-Description: Controller Helper Functions to use modals-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.Modal.ControllerFunctions (setModal) where--import IHP.Prelude-import IHP.Controller.Context-import IHP.Modal.Types-import qualified IHP.ViewSupport as ViewSupport--setModal :: (?context :: ControllerContext, ViewSupport.View view) => view -> IO ()-setModal view = do- html <- do- context <- freeze ?context- let ?context = context- let ?view = view- pure $ ViewSupport.html view- - putContext (ModalContainer html)
@@ -1,21 +0,0 @@-{-|-Module: IHP.Modal.Types-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.Modal.Types-( Modal (..)-, ModalContainer (..)-) where--import IHP.Prelude-import Text.Blaze.Html5 (Html)--data Modal = Modal- { modalContent :: Html- , modalFooter :: Maybe Html- , modalCloseUrl :: Text- , modalTitle :: Text- }---- | Stores the current modal inside @?context@-newtype ModalContainer = ModalContainer Html
@@ -1,56 +0,0 @@-{-|-Module: IHP.Modal.ViewFunctions-Description: View Helper Functions to use modals-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.Modal.ViewFunctions (modal, renderModal) where--import IHP.Prelude-import IHP.Controller.Context-import IHP.HSX.QQ (hsx)-import IHP.Modal.Types-import Text.Blaze.Html5 (Html)--renderModal modal = renderModal' modal True-renderModal' Modal { .. } show = [hsx|- <div class={modalClassName} id="modal" tabindex="-1" role="dialog" aria-labelledby="modal-title" aria-hidden="true" style={displayStyle} onclick="if (event.target.id === 'modal') document.getElementById('modal-backdrop').click()">- {modalInner}- </div>- <a id="modal-backdrop" href={modalCloseUrl} class={backdropClassName} style={displayStyle}/>- |]- where- modalClassName :: Text- modalClassName = "modal fade overflow-auto " <> if show then "show" else ""- backdropClassName :: Text- backdropClassName = "modal-backdrop fade " <> if show then "show" else ""- displayStyle :: Text- displayStyle = if show then "display: block" else "display: none"-- modalInner = [hsx|- <div class="modal-dialog" role="document" id="modal-inner">- <div class="modal-content">- {renderModalHeader modalTitle modalCloseUrl}- <div class="modal-body">{modalContent}</div>- {renderModalFooter}- </div>- </div>- |]-- renderModalFooter =- case modalFooter of- Just modalFooter -> [hsx|<div class="modal-footer">{modalFooter}</div>|]- Nothing -> mempty--renderModalHeader :: Text -> Text -> Html-renderModalHeader title closeUrl = [hsx|- <div class="modal-header">- <h5 class="modal-title" id="modal-title">{title}</h5>- <a href={closeUrl} class="btn-close" data-dismiss="modal" aria-label="Close"></a>- </div>-|]--modal :: (?context :: ControllerContext) => Html-modal =- case maybeFromFrozenContext of- Just (ModalContainer html) -> html- Nothing -> mempty
@@ -2,182 +2,122 @@ module IHP.ModelSupport ( module IHP.ModelSupport-, module IHP.Postgres.Point-, module IHP.Postgres.Polygon-, module IHP.Postgres.Inet-, module IHP.Postgres.TSVector-, module IHP.Postgres.TimeParser+, module IHP.ModelSupport.Types+, module PostgresqlTypes.Point+, module PostgresqlTypes.Polygon+, module PostgresqlTypes.Inet+, module PostgresqlTypes.Tsvector+, module PostgresqlTypes.Interval+, module IHP.InputValue ) where +import IHP.ModelSupport.Types+ import IHP.HaskellSupport-import IHP.NameSupport-import ClassyPrelude hiding (UTCTime, find, ModifiedJulianDay)-import Database.PostgreSQL.Simple (Connection)-import Database.PostgreSQL.Simple.Types (Query)-import Database.PostgreSQL.Simple.FromField hiding (Field, name)-import Database.PostgreSQL.Simple.ToField+import IHP.InputValue+import Prelude+import Data.ByteString (ByteString)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Int (Int64)+import Data.IORef (IORef, newIORef, modifyIORef')+import Control.Exception (bracket, finally, throwIO, SomeException, try, mask)+import Data.Maybe (fromMaybe, isNothing, isJust)+import Data.String (IsString(..))+import Database.PostgreSQL.Simple.Types (Query(..)) import Data.Default-import Data.Time.Format.ISO8601 (iso8601Show)+import Data.Bits ((.&.), bit, clearBit)+import Data.List (elemIndex) import Data.String.Conversions (cs ,ConvertibleStrings) import Data.Time.Clock import Data.Time.LocalTime import Data.Time.Calendar import Data.UUID import qualified Database.PostgreSQL.Simple as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Database.PostgreSQL.Simple.FromRow as PGFR-import qualified Database.PostgreSQL.Simple.ToField as PG-import qualified Database.PostgreSQL.Simple.ToRow as PG import GHC.Records import GHC.TypeLits-import GHC.Types import Data.Proxy import Data.Data import Data.Aeson (ToJSON (..), FromJSON (..)) import qualified Data.Aeson as Aeson import qualified Data.Set as Set import qualified Text.Read as Read-import qualified Data.Pool as Pool-import qualified GHC.Conc-import IHP.Postgres.Point-import IHP.Postgres.Interval ()-import IHP.Postgres.Polygon-import IHP.Postgres.Inet ()-import IHP.Postgres.TSVector-import IHP.Postgres.TimeParser+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Pool.Config as HasqlPoolConfig+import qualified Hasql.Connection.Settings as HasqlSettings+import qualified Hasql.Session as Hasql+import qualified Hasql.Statement as Hasql+import qualified Hasql.Errors as HasqlErrors+import qualified Hasql.DynamicStatements.Snippet as Snippet+import qualified Hasql.Decoders as Decoders+import qualified Hasql.Encoders as Encoders+import qualified Hasql.Implicits.Encoders+import PostgresqlTypes.Point+import PostgresqlTypes.Polygon+import PostgresqlTypes.Inet+import PostgresqlTypes.Interval+import PostgresqlTypes.Tsvector import IHP.Log.Types import qualified IHP.Log as Log import Data.Dynamic+import IHP.EnvVar import Data.Scientific import GHC.Stack-import qualified Numeric-import qualified Data.Text.Encoding as Text-import qualified Data.ByteString.Builder as Builder---- | Provides the db connection and some IHP-specific db configuration-data ModelContext = ModelContext- { connectionPool :: Pool.Pool Connection -- ^ Used to get database connections when no 'transactionConnection' is set- , transactionConnection :: Maybe Connection -- ^ Set to a specific database connection when executing a database transaction- -- | Logs all queries to this logger at log level info- , logger :: Logger- -- | A callback that is called whenever a specific table is accessed using a SELECT query- , trackTableReadCallback :: Maybe (ByteString -> IO ())- -- | Is set to a value if row level security was enabled at runtime- , rowLevelSecurity :: Maybe RowLevelSecurityContext- }---- | When row level security is enabled at runtime, this keeps track of the current--- logged in user and the postgresql role to switch to.-data RowLevelSecurityContext = RowLevelSecurityContext- { rlsAuthenticatedRole :: Text -- ^ Default is @ihp_authenticated@. This value comes from the @IHP_RLS_AUTHENTICATED_ROLE@ env var.- , rlsUserId :: PG.Action -- ^ The user id of the current logged in user- }+import qualified Hasql.Transaction as Tx+import qualified Hasql.Transaction.Sessions as Tx+import Data.Functor.Contravariant (contramap)+import Control.Concurrent (forkIO, MVar, newEmptyMVar, putMVar, takeMVar)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.Error.Class (catchError)+import IHP.Hasql.FromRow (FromRowHasql(..), HasqlDecodeColumn(..))+import IHP.Hasql.Encoders (ToSnippetParams(..), sqlToSnippet)+import IHP.Hasql.Pool (usePoolWithRetry)+import IHP.PGSimpleCompat () -- | Provides a mock ModelContext to be used when a database connection is not available notConnectedModelContext :: Logger -> ModelContext notConnectedModelContext logger = ModelContext- { connectionPool = error "Not connected"- , transactionConnection = Nothing+ { hasqlPool = error "Not connected"+ , transactionRunner = Nothing , logger = logger , trackTableReadCallback = Nothing , rowLevelSecurity = Nothing } -createModelContext :: NominalDiffTime -> Int -> ByteString -> Logger -> IO ModelContext-createModelContext idleTime maxConnections databaseUrl logger = do- let poolConfig = Pool.defaultPoolConfig (PG.connectPostgreSQL databaseUrl) PG.close (realToFrac idleTime) maxConnections- connectionPool <- Pool.newPool poolConfig+createModelContext :: ByteString -> Logger -> IO ModelContext+createModelContext databaseUrl logger = do+ -- Create hasql pool for prepared statement-based queries+ -- HASQL_POOL_SIZE: pool size (default: 20). Set to 1 for consistent prepared statement caching.+ -- HASQL_IDLE_TIME: seconds before idle connection is closed (default: 600 = 10 min)+ hasqlPoolSize :: Maybe Int <- envOrNothing "HASQL_POOL_SIZE"+ hasqlIdleTime :: Maybe Int <- envOrNothing "HASQL_IDLE_TIME"+ let hasqlPoolSettings =+ [ HasqlPoolConfig.staticConnectionSettings (HasqlSettings.connectionString (cs databaseUrl))+ ]+ <> maybe [HasqlPoolConfig.size 20] (\size -> [HasqlPoolConfig.size size]) hasqlPoolSize+ <> maybe [] (\idle -> [HasqlPoolConfig.idlenessTimeout (fromIntegral idle)]) hasqlIdleTime+ let hasqlPoolConfig = HasqlPoolConfig.settings hasqlPoolSettings+ hasqlPool <- HasqlPool.acquire hasqlPoolConfig let trackTableReadCallback = Nothing- let transactionConnection = Nothing+ let transactionRunner = Nothing let rowLevelSecurity = Nothing pure ModelContext { .. } -type family GetModelById id :: Type where- GetModelById (Maybe (Id' tableName)) = Maybe (GetModelByTableName tableName)- GetModelById (Id' tableName) = GetModelByTableName tableName-type family GetTableName model :: Symbol-type family GetModelByTableName (tableName :: Symbol) :: Type--class CanCreate a where- create :: (?modelContext :: ModelContext) => a -> IO a- createMany :: (?modelContext :: ModelContext) => [a] -> IO [a]- - -- | Like 'createRecord' but doesn't return the created record- createRecordDiscardResult :: (?modelContext :: ModelContext) => a -> IO ()- createRecordDiscardResult record = do- _ <- createRecord record- pure ()--class CanUpdate a where- updateRecord :: (?modelContext :: ModelContext) => a -> IO a+releaseModelContext :: ModelContext -> IO ()+releaseModelContext modelContext = do+ HasqlPool.release modelContext.hasqlPool - -- | Like 'updateRecord' but doesn't return the updated record- updateRecordDiscardResult :: (?modelContext :: ModelContext) => a -> IO ()- updateRecordDiscardResult record = do- _ <- updateRecord record- pure ()+-- | Bracket-style wrapper around 'createModelContext' that ensures the database+-- pool is released when the callback completes (or throws an exception).+withModelContext :: ByteString -> Logger -> (ModelContext -> IO a) -> IO a+withModelContext databaseUrl logger =+ bracket (createModelContext databaseUrl logger) releaseModelContext {-# INLINE createRecord #-} createRecord :: (?modelContext :: ModelContext, CanCreate model) => model -> IO model createRecord = create -class InputValue a where- inputValue :: a -> Text--instance InputValue Text where- inputValue text = text--instance InputValue Int where- inputValue = tshow--instance InputValue Integer where- inputValue = tshow--instance InputValue Double where- inputValue double = cs (Numeric.showFFloat Nothing double "")--instance InputValue Float where- inputValue float = cs (Numeric.showFFloat Nothing float "")--instance InputValue Bool where- inputValue True = "on"- inputValue False = "off"--instance InputValue Data.UUID.UUID where- inputValue = Data.UUID.toText--instance InputValue () where- inputValue () = "error: inputValue(()) not supported"--instance InputValue UTCTime where- inputValue time = cs (iso8601Show time)--instance InputValue LocalTime where- inputValue time = cs (iso8601Show time)--instance InputValue Day where- inputValue date = cs (iso8601Show date)--instance InputValue TimeOfDay where- inputValue timeOfDay = tshow timeOfDay--instance InputValue PGInterval where- inputValue (PGInterval pgInterval) = tshow pgInterval--instance InputValue fieldType => InputValue (Maybe fieldType) where- inputValue (Just value) = inputValue value- inputValue Nothing = ""--instance InputValue value => InputValue [value] where- inputValue list = list |> map inputValue |> intercalate ","--instance InputValue Aeson.Value where- inputValue json = json |> Aeson.encode |> cs--instance InputValue Scientific where- inputValue = tshow- instance Default Text where {-# INLINE def #-} def = ""@@ -189,19 +129,17 @@ #endif instance Default Point where- def = Point def def+ def = fromCoordinates 0 0 instance Default Polygon where- def = Polygon [def]+ def = fromMaybe (error "Default Polygon: impossible") (refineFromPointList [(0,0), (0,0), (0,0)]) -instance Default TSVector where- def = TSVector def+instance Default Tsvector where+ def = normalizeFromLexemeList [] instance Default Scientific where def = 0 -type FieldName = ByteString- -- | Returns @True@ when the record has not been saved to the database yet. Returns @False@ otherwise. -- -- __Example:__ Returns @True@ when a record has not been inserted yet.@@ -225,22 +163,6 @@ isNew model = isNothing model.meta.originalDatabaseRecord {-# INLINABLE isNew #-} -type family GetModelName model :: Symbol---- | Provides the primary key type for a given table. The instances are usually declared--- by the generated haskell code in Generated.Types------ __Example:__ Defining the primary key for a users table------ > type instance PrimaryKey "users" = UUID--------- __Example:__ Defining the primary key for a table with a SERIAL pk------ > type instance PrimaryKey "projects" = Int----type family PrimaryKey (tableName :: Symbol)- -- | Returns the model name of a given model as Text -- -- __Example:__@@ -254,19 +176,6 @@ getModelName = cs $! symbolVal (Proxy :: Proxy (GetModelName model)) {-# INLINE getModelName #-} -newtype Id' table = Id (PrimaryKey table)--deriving instance (Eq (PrimaryKey table)) => Eq (Id' table)-deriving instance (Ord (PrimaryKey table)) => Ord (Id' table)-deriving instance (Hashable (PrimaryKey table)) => Hashable (Id' table)-deriving instance (KnownSymbol table, Data (PrimaryKey table)) => Data (Id' table)-deriving instance (KnownSymbol table, NFData (PrimaryKey table)) => NFData (Id' table)---- | We need to map the model to its table name to prevent infinite recursion in the model data definition--- E.g. `type Project = Project' { id :: Id Project }` will not work--- But `type Project = Project' { id :: Id "projects" }` will-type Id model = Id' (GetTableName model)- instance InputValue (PrimaryKey model') => InputValue (Id' model') where {-# INLINE inputValue #-} inputValue = inputValue . unpackId@@ -278,19 +187,9 @@ recordToInputValue entity = entity.id |> unpackId- |> tshow+ |> Text.pack . show {-# INLINE recordToInputValue #-} -instance FromField (PrimaryKey model) => FromField (Id' model) where- {-# INLINE fromField #-}- fromField value metaData = do- fieldValue <- fromField value metaData- pure (Id fieldValue)--instance ToField (PrimaryKey model) => ToField (Id' model) where- {-# INLINE toField #-}- toField = toField . unpackId- instance Show (PrimaryKey model) => Show (Id' model) where {-# INLINE show #-} show = show . unpackId@@ -311,13 +210,6 @@ unpackId :: Id' model -> PrimaryKey model unpackId (Id uuid) = uuid --- | Record type for objects of model types labeled with values from different database tables. (e.g. comments labeled with the IDs of the posts they belong to).-data LabeledData a b = LabeledData { labelValue :: a, contentValue :: b }- deriving (Show)--instance (FromField label, PG.FromRow a) => PGFR.FromRow (LabeledData label a) where- fromRow = LabeledData <$> PGFR.field <*> PGFR.fromRow- -- | Sometimes you have a hardcoded UUID value which represents some record id. This instance allows you -- to write the Id like a string: --@@ -326,9 +218,6 @@ fromString uuid = textToId uuid {-# INLINE fromString #-} -class ParsePrimaryKey primaryKey where- parsePrimaryKey :: Text -> Maybe primaryKey- instance ParsePrimaryKey UUID where parsePrimaryKey = Read.readMaybe . cs @@ -352,19 +241,6 @@ Nothing -> error (cs $ "Unable to convert " <> (cs text :: Text) <> " to Id value. Is it a valid uuid?") {-# INLINE textToId #-} --- | Measure and log the query time for a given query action if the log level is Debug.--- If the log level is greater than debug, just perform the query action without measuring time.-measureTimeIfLogging :: (?modelContext :: ModelContext, PG.ToRow q) => Text -> PG.Connection -> IO a -> Query -> q -> IO a-measureTimeIfLogging logPrefix connection queryAction theQuery theParameters = do- let currentLogLevel = ?modelContext.logger.level- if currentLogLevel == Debug- then do- start <- getCurrentTime- queryAction `finally` do- end <- getCurrentTime- let theTime = end `diffUTCTime` start- logQuery logPrefix connection theQuery theParameters theTime- else queryAction -- | Runs a raw sql query --@@ -378,10 +254,11 @@ -- -- Use 'sqlQuerySingleRow' if you expect only a single row to be returned. ---sqlQuery :: (?modelContext :: ModelContext, PG.ToRow q, PG.FromRow r) => Query -> q -> IO [r]+sqlQuery :: (?modelContext :: ModelContext, ToSnippetParams q, FromRowHasql r) => Query -> q -> IO [r] sqlQuery theQuery theParameters = do- withDatabaseConnection \connection -> enhanceSqlError theQuery theParameters do- withRLSParams (\theQuery theParameters -> measureTimeIfLogging "🔍" connection (PG.query connection theQuery theParameters) theQuery theParameters) theQuery theParameters+ let pool = ?modelContext.hasqlPool+ let snippet = sqlToSnippet (fromQuery theQuery) (toSnippetParams theParameters)+ sqlQueryHasql pool snippet (Decoders.rowList hasqlRowDecoder) {-# INLINABLE sqlQuery #-} @@ -397,7 +274,7 @@ -- -- *AutoRefresh:* When using 'sqlQuerySingleRow' with AutoRefresh, you need to use 'trackTableRead' to let AutoRefresh know that you have accessed a certain table. Otherwise AutoRefresh will not watch table of your custom sql query. ---sqlQuerySingleRow :: (?modelContext :: ModelContext, PG.ToRow query, PG.FromRow record) => Query -> query -> IO record+sqlQuerySingleRow :: (?modelContext :: ModelContext, ToSnippetParams query, FromRowHasql record) => Query -> query -> IO record sqlQuerySingleRow theQuery theParameters = do result <- sqlQuery theQuery theParameters case result of@@ -411,10 +288,11 @@ -- __Example:__ -- -- > sqlExec "CREATE TABLE users ()" ()-sqlExec :: (?modelContext :: ModelContext, PG.ToRow q) => Query -> q -> IO Int64+sqlExec :: (?modelContext :: ModelContext, ToSnippetParams q) => Query -> q -> IO Int64 sqlExec theQuery theParameters = do- withDatabaseConnection \connection -> enhanceSqlError theQuery theParameters do- withRLSParams (\theQuery theParameters -> measureTimeIfLogging "💾" connection (PG.execute connection theQuery theParameters) theQuery theParameters) theQuery theParameters+ let pool = ?modelContext.hasqlPool+ let snippet = sqlToSnippet (fromQuery theQuery) (toSnippetParams theParameters)+ sqlExecHasqlCount pool snippet {-# INLINABLE sqlExec #-} -- | Runs a sql statement (like a CREATE statement), but doesn't return any result@@ -422,42 +300,178 @@ -- __Example:__ -- -- > sqlExecDiscardResult "CREATE TABLE users ()" ()-sqlExecDiscardResult :: (?modelContext :: ModelContext, PG.ToRow q) => Query -> q -> IO ()+sqlExecDiscardResult :: (?modelContext :: ModelContext, ToSnippetParams q) => Query -> q -> IO () sqlExecDiscardResult theQuery theParameters = do- _ <- sqlExec theQuery theParameters- pure ()+ let pool = ?modelContext.hasqlPool+ let snippet = sqlToSnippet (fromQuery theQuery) (toSnippetParams theParameters)+ sqlExecHasql pool snippet {-# INLINABLE sqlExecDiscardResult #-} --- | Wraps the query with Row level security boilerplate, if a row level security context was provided++-- | Prepared statement that sets the RLS role and user id using set_config(). --+-- Uses @set_config(setting, value, is_local)@ which is a regular SQL function+-- that supports parameterized values in the extended query protocol, unlike+-- @SET LOCAL@ which is a utility command that cannot be parameterized.+--+-- The third argument @true@ makes the setting local to the current transaction,+-- equivalent to @SET LOCAL@.+setRLSConfigStatement :: Hasql.Statement (Text, Text) ()+setRLSConfigStatement = Hasql.preparable+ "SELECT set_config('role', $1, true), set_config('rls.ihp_user_id', $2, true)"+ (contramap fst (Encoders.param (Encoders.nonNullable Encoders.text))+ <> contramap snd (Encoders.param (Encoders.nonNullable Encoders.text)))+ (Decoders.singleRow (Decoders.column (Decoders.nullable Decoders.text) *> Decoders.column (Decoders.nullable Decoders.text) *> pure ()))++-- | Runs a hasql 'Hasql.Statement' directly on the pool.+--+-- Core execution function that handles session construction, RLS wrapping,+-- pool execution, cached plan error retry, and debug logging.+-- Works with both prepared ('Hasql.preparable') and unprepared statements.+-- -- __Example:__ ----- If a row level security context is given, this will turn a query like the following+-- > result <- sqlStatementHasql pool someId myPreparedStatement ----- > withRLSParams runQuery "SELECT * FROM projects WHERE id = ?" (Only "..")+sqlStatementHasql :: (?modelContext :: ModelContext) => HasqlPool.Pool -> a -> Hasql.Statement a b -> IO b+sqlStatementHasql pool input statement = do+ let ?context = ?modelContext+ let currentLogLevel = ?modelContext.logger.level+ let session = case (?modelContext.transactionRunner, ?modelContext.rowLevelSecurity) of+ (Nothing, Just RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId }) ->+ Tx.transaction Tx.ReadCommitted Tx.Read $ do+ Tx.statement (rlsAuthenticatedRole, rlsUserId) setRLSConfigStatement+ Tx.statement input statement+ _ ->+ Hasql.statement input statement+ let runQuery = case ?modelContext.transactionRunner of+ Just (TransactionRunner runner) -> runner session+ Nothing -> usePoolWithRetry pool session+ logQueryTiming currentLogLevel ("🔍 " <> truncateQuery (cs (Hasql.toSql statement))) runQuery+{-# INLINABLE sqlStatementHasql #-}++-- | Runs a query built from a dynamic 'Snippet'. ----- Into the following equivalent:+-- Converts the snippet to a 'Hasql.Statement' and delegates to 'sqlStatementHasql'. ----- > runQuery "SET LOCAL ROLE ?; SET LOCAL rls.ihp_user_id = ?; SELECT * FROM projects WHERE id = ?" ["ihp_authenticated", "<user id>", .."]+-- __Example:__ ---withRLSParams :: (?modelContext :: ModelContext, PG.ToRow params) => (PG.Query -> [PG.Action] -> result) -> PG.Query -> params -> result-withRLSParams runQuery query params = do- case ?modelContext.rowLevelSecurity of- Just RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId } -> do- let query' = "SET LOCAL ROLE ?; SET LOCAL rls.ihp_user_id = ?; " <> query- let params' = [PG.toField (PG.Identifier rlsAuthenticatedRole), PG.toField rlsUserId] <> PG.toRow params- runQuery query' params'- Nothing -> runQuery query (PG.toRow params)+-- > users <- sqlQueryHasql pool snippet (Decoders.rowList userDecoder)+--+sqlQueryHasql :: (?modelContext :: ModelContext) => HasqlPool.Pool -> Snippet.Snippet -> Decoders.Result a -> IO a+sqlQueryHasql pool snippet decoder =+ sqlStatementHasql pool () (Snippet.toPreparableStatement snippet decoder)+{-# INLINABLE sqlQueryHasql #-} -withDatabaseConnection :: (?modelContext :: ModelContext) => (Connection -> IO a) -> IO a-withDatabaseConnection block =- let- ModelContext { connectionPool, transactionConnection, rowLevelSecurity } = ?modelContext- in case transactionConnection of- Just transactionConnection -> block transactionConnection- Nothing -> Pool.withResource connectionPool block-{-# INLINABLE withDatabaseConnection #-}+-- | Like 'sqlStatementHasql' but for write operations (DELETE, UPDATE, INSERT without results).+--+-- Uses 'Tx.Write' for RLS transactions (vs 'Tx.Read' in 'sqlStatementHasql').+-- Takes a 'Hasql.Statement' directly — use this when you have a pre-built statement+-- rather than a 'Snippet'.+--+-- __Example:__+--+-- > sqlExecStatement pool workerId myUpdateStatement+--+sqlExecStatement :: (?modelContext :: ModelContext) => HasqlPool.Pool -> a -> Hasql.Statement a () -> IO ()+sqlExecStatement pool input statement = do+ let ?context = ?modelContext+ let currentLogLevel = ?modelContext.logger.level+ let session = case (?modelContext.transactionRunner, ?modelContext.rowLevelSecurity) of+ (Just _, _) ->+ Hasql.statement input statement+ (Nothing, Just RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId }) ->+ Tx.transaction Tx.ReadCommitted Tx.Write $ do+ Tx.statement (rlsAuthenticatedRole, rlsUserId) setRLSConfigStatement+ Tx.statement input statement+ _ ->+ Hasql.statement input statement+ let runQuery = case ?modelContext.transactionRunner of+ Just (TransactionRunner runner) -> runner session+ Nothing -> usePoolWithRetry pool session+ logQueryTiming currentLogLevel ("💾 " <> truncateQuery (cs (Hasql.toSql statement))) runQuery+{-# INLINABLE sqlExecStatement #-} +-- | Like 'sqlQueryHasql' but for statements that don't return results (DELETE, etc.)+--+-- When RLS is enabled, the statement is wrapped in a transaction that first sets the+-- role and user id via 'setRLSConfigStatement'.+sqlExecHasql :: (?modelContext :: ModelContext) => HasqlPool.Pool -> Snippet.Snippet -> IO ()+sqlExecHasql pool snippet = sqlExecStatement pool () (Snippet.toPreparableStatement snippet Decoders.noResult)+{-# INLINABLE sqlExecHasql #-}++-- | Like 'sqlExecHasql' but returns the number of affected rows+--+-- When RLS is enabled, the statement is wrapped in a transaction that first sets the+-- role and user id via 'setRLSConfigStatement'.+sqlExecHasqlCount :: (?modelContext :: ModelContext) => HasqlPool.Pool -> Snippet.Snippet -> IO Int64+sqlExecHasqlCount pool snippet = do+ let ?context = ?modelContext+ let currentLogLevel = ?modelContext.logger.level+ let statement = Snippet.toPreparableStatement snippet Decoders.rowsAffected+ let session = case (?modelContext.transactionRunner, ?modelContext.rowLevelSecurity) of+ (Nothing, Just RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId }) ->+ Tx.transaction Tx.ReadCommitted Tx.Write $ do+ Tx.statement (rlsAuthenticatedRole, rlsUserId) setRLSConfigStatement+ Tx.statement () statement+ _ ->+ Hasql.statement () statement+ let runQuery = case ?modelContext.transactionRunner of+ Just (TransactionRunner runner) -> runner session+ Nothing -> usePoolWithRetry pool session+ logQueryTiming currentLogLevel ("💾 " <> cs (Hasql.toSql statement)) runQuery+{-# INLINABLE sqlExecHasqlCount #-}++-- | Like 'sqlExecHasql' but for raw 'Hasql.Session' values (e.g. multi-statement DDL via 'Hasql.sql')+--+-- Use this instead of 'sqlExecHasql' when you need the simple protocol (no prepared statements),+-- e.g. for multi-statement SQL like trigger creation.+--+-- __Example:__+--+-- > runSessionHasql pool (Hasql.sql "BEGIN; CREATE ...; COMMIT;")+--+runSessionHasql :: (?modelContext :: ModelContext) => HasqlPool.Pool -> Hasql.Session () -> IO ()+runSessionHasql pool session = do+ let ?context = ?modelContext+ let currentLogLevel = ?modelContext.logger.level+ let runQuery = case ?modelContext.transactionRunner of+ Just (TransactionRunner runner) -> runner session+ Nothing -> usePoolWithRetry pool session+ logQueryTiming currentLogLevel "💾 runSessionHasql" runQuery+{-# INLINABLE runSessionHasql #-}+++-- | Run an IO action, logging its duration when the log level is 'Debug'.+-- The label is prepended to the timing message, e.g. @"🔍 SELECT ..."@.+{-# INLINE logQueryTiming #-}+logQueryTiming :: (?context :: ModelContext) => LogLevel -> Text -> IO a -> IO a+logQueryTiming currentLogLevel label runQuery =+ if currentLogLevel == Debug+ then do+ start <- getCurrentTime+ runQuery `finally` do+ end <- getCurrentTime+ let queryTimeInMs = round (realToFrac (end `diffUTCTime` start) * 1000 :: Double) :: Int+ Log.debug (label <> " (" <> Text.pack (show queryTimeInMs) <> "ms)")+ else runQuery++-- | Existential wrapper for sub-session requests in a transaction+data SessionRequest where+ SessionRequest :: Hasql.Session a -> MVar (Either HasqlErrors.SessionError a) -> SessionRequest++-- | Loop that reads sub-session requests from an MVar and executes them+-- on the current transaction's connection. Stops when it receives 'Nothing'.+processRequests :: MVar (Maybe SessionRequest) -> Hasql.Session ()+processRequests requestMVar = do+ req <- liftIO (takeMVar requestMVar)+ case req of+ Just (SessionRequest session responseVar) -> do+ result <- catchError (Right <$> session) (pure . Left)+ liftIO (putMVar responseVar result)+ processRequests requestMVar+ Nothing -> pure ()+ -- | Runs a raw sql query which results in a single scalar value such as an integer or string -- -- __Example:__@@ -465,7 +479,7 @@ -- > usersCount <- sqlQueryScalar "SELECT COUNT(*) FROM users" -- -- Take a look at "IHP.QueryBuilder" for a typesafe approach on building simple queries.-sqlQueryScalar :: (?modelContext :: ModelContext) => (PG.ToRow q, FromField value) => Query -> q -> IO value+sqlQueryScalar :: (?modelContext :: ModelContext, ToSnippetParams q, HasqlDecodeColumn value) => Query -> q -> IO value sqlQueryScalar theQuery theParameters = do result <- sqlQuery theQuery theParameters pure case result of@@ -480,7 +494,7 @@ -- > usersCount <- sqlQueryScalarOrNothing "SELECT COUNT(*) FROM users" -- -- Take a look at "IHP.QueryBuilder" for a typesafe approach on building simple queries.-sqlQueryScalarOrNothing :: (?modelContext :: ModelContext) => (PG.ToRow q, FromField value) => Query -> q -> IO (Maybe value)+sqlQueryScalarOrNothing :: (?modelContext :: ModelContext, ToSnippetParams q, HasqlDecodeColumn value) => Query -> q -> IO (Maybe value) sqlQueryScalarOrNothing theQuery theParameters = do result <- sqlQuery theQuery theParameters pure case result of@@ -505,12 +519,54 @@ -- > |> set #ownerId user.id -- > |> updateRecord withTransaction :: (?modelContext :: ModelContext) => ((?modelContext :: ModelContext) => IO a) -> IO a-withTransaction block = withTransactionConnection do- let connection = ?modelContext.transactionConnection- |> \case- Just connection -> connection- Nothing -> error "withTransaction: transactionConnection not set as expected"- PG.withTransaction connection block+withTransaction block+ | isJust ?modelContext.transactionRunner =+ error "withTransaction: Nested transactions are not supported. withTransaction was called inside an existing transaction."+ | otherwise = do+ let pool = ?modelContext.hasqlPool+ requestMVar <- newEmptyMVar++ let runner :: forall a. Hasql.Session a -> IO a+ runner session = do+ responseVar <- newEmptyMVar+ putMVar requestMVar (Just (SessionRequest session responseVar))+ result <- takeMVar responseVar+ case result of+ Left err -> throwIO (HasqlSessionError err)+ Right a -> pure a++ let ?modelContext = ?modelContext { transactionRunner = Just (TransactionRunner runner) }++ let ?context = ?modelContext+ let transactionSession = do+ Hasql.script "BEGIN"+ case ?modelContext.rowLevelSecurity of+ Just RowLevelSecurityContext { rlsAuthenticatedRole, rlsUserId } ->+ Hasql.statement (rlsAuthenticatedRole, rlsUserId) setRLSConfigStatement+ Nothing -> pure ()++ -- Fork the user's block in a separate thread+ blockResultVar <- liftIO $ do+ resultVar <- newEmptyMVar+ _ <- forkIO $ mask \restore -> do+ result <- try @SomeException (restore block)+ putMVar requestMVar Nothing -- Signal processRequests to stop+ putMVar resultVar result+ pure resultVar++ processRequests requestMVar++ blockResult <- liftIO (takeMVar blockResultVar)+ case blockResult of+ Left exc -> do+ catchError (Hasql.script "ROLLBACK") (\rollbackErr -> liftIO $+ Log.warn ("withTransaction: ROLLBACK failed: " <> Text.pack (show rollbackErr)))+ liftIO (throwIO exc)+ Right a -> do+ Hasql.script "COMMIT"+ pure a++ usePoolWithRetry pool transactionSession {-# INLINABLE withTransaction #-} -- | Executes the given block with the main database role and temporarly sidesteps the row level security policies.@@ -534,31 +590,18 @@ let ?modelContext = currentModelContext { rowLevelSecurity = Nothing } in block {-# INLINABLE withRowLevelSecurityDisabled #-} --- | Returns the postgres connection when called within a 'withTransaction' block------ Throws an error if called from outside a 'withTransaction'-transactionConnectionOrError :: (?modelContext :: ModelContext) => Connection-transactionConnectionOrError = ?modelContext.transactionConnection- |> \case- Just connection -> connection- Nothing -> error "getTransactionConnectionOrError: Not in a transaction state"- commitTransaction :: (?modelContext :: ModelContext) => IO ()-commitTransaction = PG.commit transactionConnectionOrError+commitTransaction = case ?modelContext.transactionRunner of+ Just (TransactionRunner runner) -> runner (Hasql.script "COMMIT")+ Nothing -> error "commitTransaction: Not in a transaction" {-# INLINABLE commitTransaction #-} rollbackTransaction :: (?modelContext :: ModelContext) => IO ()-rollbackTransaction = PG.rollback transactionConnectionOrError+rollbackTransaction = case ?modelContext.transactionRunner of+ Just (TransactionRunner runner) -> runner (Hasql.script "ROLLBACK")+ Nothing -> error "rollbackTransaction: Not in a transaction" {-# INLINABLE rollbackTransaction #-} -withTransactionConnection :: (?modelContext :: ModelContext) => ((?modelContext :: ModelContext) => IO a) -> IO a-withTransactionConnection block = do- let ModelContext { connectionPool } = ?modelContext- Pool.withResource connectionPool \connection -> do- let modelContext = ?modelContext { transactionConnection = Just connection }- let ?modelContext = modelContext in block-{-# INLINABLE withTransactionConnection #-}- -- | Access meta data for a database table class ( KnownSymbol (GetTableName record)@@ -575,17 +618,6 @@ tableName = symbolToText @(GetTableName record) {-# INLINE tableName #-} - -- | Returns the table name of a given model as a bytestring.- --- -- __Example:__- --- -- >>> tableNameByteString @User- -- "users"- --- tableNameByteString :: ByteString- tableNameByteString = Text.encodeUtf8 (tableName @record)- {-# INLINE tableNameByteString #-}- -- | Returns the list of column names for a given model -- -- __Example:__@@ -593,7 +625,7 @@ -- >>> columnNames @User -- ["id", "email", "created_at"] --- columnNames :: [ByteString]+ columnNames :: [Text] -- | Returns the list of column names, that are contained in the primary key for a given model --@@ -605,22 +637,8 @@ -- >>> primaryKeyColumnNames @PostTagging -- ["post_id", "tag_id"] --- primaryKeyColumnNames :: [ByteString]+ primaryKeyColumnNames :: [Text] - -- | Returns the parameters for a WHERE conditions to match an entity by it's primary key, given the entities id- --- -- For tables with a simple primary key this simply the id:- --- -- >>> primaryKeyConditionForId project.id- -- Plain "d619f3cf-f355-4614-8a4c-e9ea4f301e39"- --- -- If the table has a composite primary key, this returns multiple elements:- --- -- >>> primaryKeyConditionForId postTag.id- -- Many [Plain "(", Plain "0ace9270-568f-4188-b237-3789aa520588", Plain ",", Plain "0b58fdf5-4bbb-4e57-a5b7-aa1c57148e1c", Plain ")"]- -- - -- The order of the elements for a composite primary key must match the order of the columns returned by 'primaryKeyColumnNames'- primaryKeyConditionForId :: Id record -> PG.Action -- | Returns ByteString, that represents the part of an SQL where clause, that matches on a tuple consisting of all the primary keys -- For table with simple primary keys this simply returns the name of the primary key column, without wrapping in a tuple@@ -628,45 +646,21 @@ -- "(post_tags.post_id, post_tags.tag_id)" -- >>> primaryKeyColumnSelector @Post -- "post_tags.post_id"-primaryKeyConditionColumnSelector :: forall record. (Table record) => ByteString-primaryKeyConditionColumnSelector = - let - qualifyColumnName col = tableNameByteString @record <> "." <> col+primaryKeyConditionColumnSelector :: forall record. (Table record) => Text+primaryKeyConditionColumnSelector =+ let+ qualifyColumnName col = tableName @record <> "." <> col in case primaryKeyColumnNames @record of [] -> error . cs $ "Impossible happened in primaryKeyConditionColumnSelector. No primary keys found for table " <> tableName @record <> ". At least one primary key is required." [s] -> qualifyColumnName s- conds -> "(" <> intercalate ", " (map qualifyColumnName conds) <> ")"+ conds -> "(" <> Text.intercalate ", " (map qualifyColumnName conds) <> ")" --- | Returns WHERE conditions to match an entity by it's primary key------ For tables with a simple primary key this returns a tuple with the id:------ >>> primaryKeyCondition project--- Plain "d619f3cf-f355-4614-8a4c-e9ea4f301e39"------ If the table has a composite primary key, this returns multiple elements:------ >>> primaryKeyCondition postTag--- Many [Plain "(", Plain "0ace9270-568f-4188-b237-3789aa520588", Plain ",", Plain "0b58fdf5-4bbb-4e57-a5b7-aa1c57148e1c", Plain ")"]-primaryKeyCondition :: forall record. (HasField "id" record (Id record), Table record) => record -> PG.Action-primaryKeyCondition record = primaryKeyConditionForId @record record.id -logQuery :: (?modelContext :: ModelContext, PG.ToRow parameters) => Text -> PG.Connection -> Query -> parameters -> NominalDiffTime -> IO ()-logQuery logPrefix connection query parameters time = do- let ?context = ?modelContext- -- NominalTimeDiff is represented as seconds, and doesn't provide a FormatTime option for printing in ms.- -- To get around that we convert to and from a rational so we can format as desired.- let queryTimeInMs = (time * 1000) |> toRational |> fromRational @Double |> round- let formatRLSInfo userId = " { ihp_user_id = " <> userId <> " }"- let rlsInfo = case ?context.rowLevelSecurity of- Just RowLevelSecurityContext { rlsUserId = PG.Plain rlsUserId } -> formatRLSInfo (cs (Builder.toLazyByteString rlsUserId))- Just RowLevelSecurityContext { rlsUserId = rlsUserId } -> formatRLSInfo (tshow rlsUserId)- Nothing -> ""-- formatted <- PG.formatQuery connection query parameters- Log.debug (logPrefix <> " " <> cs formatted <> rlsInfo <> " (" <> tshow queryTimeInMs <> "ms)")-{-# INLINABLE logQuery #-}+truncateQuery :: Text -> Text+truncateQuery query+ | Text.length query > 2000 = Text.take 2000 query <> "... (truncated)"+ | otherwise = query -- | Runs a @DELETE@ query for a record. --@@ -676,7 +670,7 @@ -- -- Use 'deleteRecords' if you want to delete multiple records. ---deleteRecord :: forall record table. (?modelContext :: ModelContext, Table record, Show (PrimaryKey table), HasField "id" record (Id record), GetTableName record ~ table, record ~ GetModelByTableName table) => record -> IO ()+deleteRecord :: forall record table. (?modelContext :: ModelContext, Table record, Show (PrimaryKey table), HasField "id" record (Id record), GetTableName record ~ table, record ~ GetModelByTableName table, Hasql.Implicits.Encoders.DefaultParamEncoder (Id' table)) => record -> IO () deleteRecord record = deleteRecordById @record record.id {-# INLINABLE deleteRecord #-}@@ -687,12 +681,12 @@ -- >>> delete projectId -- DELETE FROM projects WHERE id = '..' ---deleteRecordById :: forall record table. (?modelContext :: ModelContext, Table record, Show (PrimaryKey table), GetTableName record ~ table, record ~ GetModelByTableName table) => Id' table -> IO ()+deleteRecordById :: forall record table. (?modelContext :: ModelContext, Table record, Show (PrimaryKey table), GetTableName record ~ table, record ~ GetModelByTableName table, Hasql.Implicits.Encoders.DefaultParamEncoder (Id' table)) => Id' table -> IO () deleteRecordById id = do- let theQuery = "DELETE FROM " <> tableNameByteString @record <> " WHERE " <> (primaryKeyConditionColumnSelector @record) <> " = ?"- let theParameters = PG.Only $ primaryKeyConditionForId @record id- sqlExec (PG.Query $! theQuery) theParameters- pure ()+ let pool = ?modelContext.hasqlPool+ sqlExecHasql pool $+ Snippet.sql ("DELETE FROM " <> tableName @record <> " WHERE " <> primaryKeyConditionColumnSelector @record <> " = ")+ <> Snippet.param id {-# INLINABLE deleteRecordById #-} -- | Runs a @DELETE@ query for a list of records.@@ -700,7 +694,7 @@ -- >>> let projects :: [Project] = ... -- >>> deleteRecords projects -- DELETE FROM projects WHERE id IN (..)-deleteRecords :: forall record table. (?modelContext :: ModelContext, Show (PrimaryKey table), Table record, HasField "id" record (Id' table), GetTableName record ~ table, record ~ GetModelByTableName table) => [record] -> IO ()+deleteRecords :: forall record table. (?modelContext :: ModelContext, Show (PrimaryKey table), Table record, HasField "id" record (Id' table), GetTableName record ~ table, record ~ GetModelByTableName table, Hasql.Implicits.Encoders.DefaultParamEncoder [Id' table]) => [record] -> IO () deleteRecords records = deleteRecordByIds @record (ids records) {-# INLINABLE deleteRecords #-}@@ -711,12 +705,13 @@ -- >>> delete projectIds -- DELETE FROM projects WHERE id IN ('..') ---deleteRecordByIds :: forall record table. (?modelContext :: ModelContext, Show (PrimaryKey table), Table record, GetTableName record ~ table, record ~ GetModelByTableName table) => [Id' table] -> IO ()+deleteRecordByIds :: forall record table. (?modelContext :: ModelContext, Show (PrimaryKey table), Table record, GetTableName record ~ table, record ~ GetModelByTableName table, Hasql.Implicits.Encoders.DefaultParamEncoder [Id' table]) => [Id' table] -> IO () deleteRecordByIds ids = do- let theQuery = "DELETE FROM " <> tableNameByteString @record <> " WHERE " <> (primaryKeyConditionColumnSelector @record) <> " IN ?"- let theParameters = PG.Only $ PG.In $ map (primaryKeyConditionForId @record) ids- sqlExec (PG.Query $! theQuery) theParameters- pure ()+ let pool = ?modelContext.hasqlPool+ sqlExecHasql pool $+ Snippet.sql ("DELETE FROM " <> tableName @record <> " WHERE " <> primaryKeyConditionColumnSelector @record <> " = ANY(")+ <> Snippet.param ids+ <> Snippet.sql ")" {-# INLINABLE deleteRecordByIds #-} -- | Runs a @DELETE@ query to delete all rows in a table.@@ -725,17 +720,10 @@ -- DELETE FROM projects deleteAll :: forall record. (?modelContext :: ModelContext, Table record) => IO () deleteAll = do- let theQuery = "DELETE FROM " <> tableName @record- sqlExec (PG.Query . cs $! theQuery) ()- pure ()+ let pool = ?modelContext.hasqlPool+ sqlExecHasql pool $ Snippet.sql ("DELETE FROM " <> tableName @record) {-# INLINABLE deleteAll #-} -type family Include (name :: GHC.Types.Symbol) model--type family Include' (name :: [GHC.Types.Symbol]) model where- Include' '[] model = model- Include' (x:xs) model = Include' xs (Include x model)- instance Default NominalDiffTime where def = 0 @@ -754,22 +742,15 @@ instance Default (PG.Binary ByteString) where def = PG.Binary "" -instance Default PGInterval where- def = PGInterval "00:00:00"+instance Default Interval where+ def = normalizeFromMonthsDaysAndMicroseconds 0 0 0 +instance Default Inet where+ def = normalizeFromV4 0 32+ class Record model where newRecord :: model --- | Helper type to deal with models where relations are included or that are only partially fetched--- Examples:------ >>> NormalizeModel (Include "author_id" Post)--- Post------ >>> NormalizeModel Post--- Post-type NormalizeModel model = GetModelByTableName (GetTableName model)- -- | Returns the ids for a list of models -- -- Shorthand for @map (.id) records@.@@ -781,34 +762,21 @@ ids records = map (.id) records {-# INLINE ids #-} --- | The error message of a validator can be either a plain text value or a HTML formatted value-data Violation- = TextViolation { message :: !Text } -- ^ Plain text validation error, like "cannot be empty"- | HtmlViolation { message :: !Text } -- ^ HTML formatted, already pre-escaped validation error, like "Invalid, please <a href="http://example.com">check the documentation</a>"- deriving (Eq, Show)---- | Every IHP database record has a magic @meta@ field which keeps a @MetaBag@ inside. This data structure is used e.g. to keep track of the validation errors that happend.-data MetaBag = MetaBag- { annotations :: ![(Text, Violation)] -- ^ Stores validation failures, as a list of (field name, error) pairs. E.g. @annotations = [ ("name", TextViolation "cannot be empty") ]@- , touchedFields :: ![Text] -- ^ Whenever a 'set' is callled on a field, it will be marked as touched. Only touched fields are saved to the database when you call 'updateRecord'- , originalDatabaseRecord :: Maybe Dynamic -- ^ When the record has been fetched from the database, we save the initial database record here. This is used by 'didChange' to check if a field value is different from the initial database value.- } deriving (Show)--instance Eq MetaBag where- MetaBag { annotations, touchedFields } == MetaBag { annotations = annotations', touchedFields = touchedFields' } = annotations == annotations' && touchedFields == touchedFields'- instance Default MetaBag where- def = MetaBag { annotations = [], touchedFields = [], originalDatabaseRecord = Nothing }+ def = MetaBag { annotations = [], touchedFields = 0, originalDatabaseRecord = Nothing } {-# INLINE def #-} instance SetField "annotations" MetaBag [(Text, Violation)] where setField value meta = meta { annotations = value } {-# INLINE setField #-} -instance SetField "touchedFields" MetaBag [Text] where+instance SetField "touchedFields" MetaBag Integer where setField value meta = meta { touchedFields = value } {-# INLINE setField #-} +-- | Returns 'True' if the named field has been touched (is in @touchedFields@).+-- Used by generated Update statement modules to determine which columns to send.+ -- | Returns 'True' if any fields of the record have unsaved changes -- -- __Example:__ Returns 'False' for freshly fetched records@@ -825,7 +793,7 @@ -- >>> project |> set #name "New Name" |> didChangeRecord -- True didChangeRecord :: (HasField "meta" record MetaBag) => record -> Bool-didChangeRecord record = isEmpty record.meta.touchedFields+didChangeRecord record = record.meta.touchedFields /= 0 -- | Returns 'True' if the specific field of the record has unsaved changes --@@ -846,7 +814,7 @@ -- __Example:__ Setting a flash message after updating the profile picture -- -- > when (user |> didChange #profilePictureUrl) (setSuccessMessage "Your Profile Picture has been updated. It might take a few minutes until it shows up everywhere")-didChange :: forall fieldName fieldValue record. (KnownSymbol fieldName, HasField fieldName record fieldValue, HasField "meta" record MetaBag, Eq fieldValue, Typeable record) => Proxy fieldName -> record -> Bool+didChange :: forall fieldName fieldValue record. (KnownSymbol fieldName, HasField fieldName record fieldValue, HasField "meta" record MetaBag, Eq fieldValue, Typeable record, FieldBit fieldName record) => Proxy fieldName -> record -> Bool didChange field record = didTouchField field record && didChangeField where didChangeField :: Bool@@ -879,70 +847,9 @@ -- >>> project |> set #name project.name |> didTouchField #name -- True ---didTouchField :: forall fieldName fieldValue record. (KnownSymbol fieldName, HasField fieldName record fieldValue, HasField "meta" record MetaBag, Eq fieldValue, Typeable record) => Proxy fieldName -> record -> Bool+didTouchField :: forall fieldName fieldValue record. (KnownSymbol fieldName, HasField fieldName record fieldValue, HasField "meta" record MetaBag, Eq fieldValue, Typeable record, FieldBit fieldName record) => Proxy fieldName -> record -> Bool didTouchField field record =- record.meta.touchedFields- |> includes (symbolToText @fieldName)---- | Represents fields that have a default value in an SQL schema------ The 'Default' constructor represents the default value from the schema,--- while the 'NonDefault' constructor holds some other value for the field-data FieldWithDefault valueType = Default | NonDefault valueType deriving (Eq, Show)--instance ToField valueType => ToField (FieldWithDefault valueType) where- toField Default = Plain "DEFAULT"- toField (NonDefault a) = toField a---- | Construct a 'FieldWithDefault'------ Use the default SQL value when the field hasn't been touched since the--- record was created. This information is stored in the 'touchedFields'--- attribute of the 'meta' field.-fieldWithDefault- :: ( KnownSymbol name- , HasField name model value- , HasField "meta" model MetaBag- )- => Proxy name- -> model- -> FieldWithDefault value-fieldWithDefault name model- | cs (symbolVal name) `elem` model.meta.touchedFields =- NonDefault (get name model)- | otherwise = Default---- | Represents fields that may have been updated------ The 'NoUpdate' constructor represents the existing value in the database,--- while the 'Update' constructor holds some new value for the field-data FieldWithUpdate name value- = NoUpdate (Proxy name)- | Update value- deriving (Eq, Show)--instance (KnownSymbol name, ToField value) => ToField (FieldWithUpdate name value) where- toField (NoUpdate name) =- Plain (ClassyPrelude.fromString $ cs $ fieldNameToColumnName $ cs $ symbolVal name)- toField (Update a) = toField a---- | Construct a 'FieldWithUpdate'------ Use the current database value when the field hasn't been touched since the--- record was accessed. This information is stored in the 'touchedFields'--- attribute of the 'meta' field.-fieldWithUpdate- :: ( KnownSymbol name- , HasField name model value- , HasField "meta" model MetaBag- )- => Proxy name- -> model- -> FieldWithUpdate name value-fieldWithUpdate name model- | cs (symbolVal name) `elem` model.meta.touchedFields =- Update (get name model)- | otherwise = NoUpdate name+ record.meta.touchedFields .&. fieldBit @fieldName @record /= 0 instance (ToJSON (PrimaryKey a)) => ToJSON (Id' a) where toJSON (Id a) = toJSON a@@ -950,43 +857,10 @@ instance (FromJSON (PrimaryKey a)) => FromJSON (Id' a) where parseJSON value = Id <$> parseJSON value --- | Thrown by 'fetchOne' when the query result is empty-data RecordNotFoundException- = RecordNotFoundException { queryAndParams :: (ByteString, [Action]) }- deriving (Show) -instance Exception RecordNotFoundException---- | Whenever calls to 'Database.PostgreSQL.Simple.query' or 'Database.PostgreSQL.Simple.execute'--- raise an 'Database.PostgreSQL.Simple.SqlError' exception, we wrap that exception in this data structure.--- This allows us to show the actual database query that has triggered the error.-data EnhancedSqlError- = EnhancedSqlError- { sqlErrorQuery :: Query- , sqlErrorQueryParams :: [Action]- , sqlError :: PG.SqlError- } deriving (Show)---- | Catches 'SqlError' and wraps them in 'EnhancedSqlError'-enhanceSqlError :: PG.ToRow parameters => Query -> parameters -> IO a -> IO a-enhanceSqlError sqlErrorQuery sqlErrorQueryParams block = catch block (\sqlError -> throwIO EnhancedSqlError { sqlErrorQuery, sqlErrorQueryParams = PG.toRow sqlErrorQueryParams, sqlError })-{-# INLINE enhanceSqlError #-}--instance Exception EnhancedSqlError- instance Default Aeson.Value where def = Aeson.Null --- | This instance allows us to avoid wrapping lists with PGArray when--- using sql types such as @INT[]@-instance ToField value => ToField [value] where- toField list = toField (PG.PGArray list)---- | This instancs allows us to avoid wrapping lists with PGArray when--- using sql types such as @INT[]@-instance (FromField value, Typeable value) => FromField [value] where- fromField field value = PG.fromPGArray <$> (fromField field value)- -- | Useful to manually mark a table read when doing a custom sql query inside AutoRefresh or 'withTableReadTracker'. -- -- When using 'fetch' on a query builder, this function is automatically called. That's why you only need to call@@ -1001,7 +875,7 @@ -- > render MyView { .. } -- ---trackTableRead :: (?modelContext :: ModelContext) => ByteString -> IO ()+trackTableRead :: (?modelContext :: ModelContext) => Text -> IO () trackTableRead tableName = case ?modelContext.trackTableReadCallback of Just callback -> callback tableName Nothing -> pure ()@@ -1020,7 +894,7 @@ -- > tables <- readIORef ?touchedTables -- > -- tables = Set.fromList ["projects", "users"] -- >-withTableReadTracker :: (?modelContext :: ModelContext) => ((?modelContext :: ModelContext, ?touchedTables :: IORef (Set ByteString)) => IO ()) -> IO ()+withTableReadTracker :: (?modelContext :: ModelContext) => ((?modelContext :: ModelContext, ?touchedTables :: IORef (Set.Set Text)) => IO ()) -> IO () withTableReadTracker trackedSection = do touchedTablesVar <- newIORef Set.empty let trackTableReadCallback = Just \tableName -> modifyIORef' touchedTablesVar (Set.insert tableName)@@ -1116,10 +990,11 @@ copyRecord :: forall record id. (Table record, SetField "id" record id, Default id, SetField "meta" record MetaBag) => record -> record copyRecord existingRecord = let- fieldsExceptId = (columnNames @record) |> filter (\field -> field /= "id")-+ columns = columnNames @record+ numColumns = length columns+ idIndex = fromMaybe 0 $ elemIndex "id" columns meta :: MetaBag- meta = def { touchedFields = map (IHP.NameSupport.columnNameToFieldName . cs) fieldsExceptId }+ meta = def { touchedFields = (bit numColumns - 1) `clearBit` idIndex } in existingRecord |> set #id def
@@ -0,0 +1,248 @@+{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleContexts, AllowAmbiguousTypes, UndecidableInstances, FlexibleInstances, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, GeneralizedNewtypeDeriving #-}++{-|+Module: IHP.ModelSupport.Types+Description: Core types for IHP's model and database support+Copyright: (c) digitally induced GmbH, 2020++This module contains the core types for IHP's model support.+It's designed to be lightweight and avoid heavy dependencies,+allowing modules that only need the types to compile faster.++For the full model API including query functions, use 'IHP.ModelSupport'.+-}+module IHP.ModelSupport.Types+( -- * Model Context+ ModelContext (..)+, RowLevelSecurityContext (..)+, TransactionRunner (..)+ -- * Type Families+, GetModelById+, GetTableName+, GetModelByTableName+, PrimaryKey+, GetModelName+, Include+, Include'+, NormalizeModel+ -- * Id Types+, Id'(..)+, Id+ -- * Record Metadata+, MetaBag (..)+, Violation (..)+, FieldName+ -- * Field Wrappers+, FieldWithDefault (..)+, FieldWithUpdate (..)+ -- * Utility Types+, LabeledData (..)+ -- * Exceptions+, RecordNotFoundException (..)+, EnhancedSqlError (..)+, enhancedSqlErrorMessage+, HasqlSessionError (..)+, HasqlError (..)+ -- * Type Classes+, CanCreate (..)+, CanUpdate (..)+, ParsePrimaryKey (..)+, FieldBit (..)+) where++import Prelude+import Data.ByteString (ByteString)+import Data.Text (Text)+import qualified Data.Text.Encoding+import Data.Hashable (Hashable)+import Control.DeepSeq (NFData)+import Control.Exception (Exception)+import Database.PostgreSQL.Simple.Types (Query)+import qualified Database.PostgreSQL.Simple as PG+import qualified Hasql.Pool as Hasql+import qualified Hasql.Session as HasqlSession+import qualified Hasql.Errors as HasqlErrors+import GHC.TypeLits+import GHC.Types+import Data.Data+import Data.Dynamic+import IHP.Log.Types (Logger)++-- | Runner that executes a hasql Session on the current transaction's connection+newtype TransactionRunner = TransactionRunner+ { runInTransaction :: forall a. HasqlSession.Session a -> IO a }++-- | Wrapper to make 'HasqlErrors.SessionError' an 'Exception', since it doesn't have one by default+data HasqlSessionError = HasqlSessionError HasqlErrors.SessionError+ deriving (Show)++instance Exception HasqlSessionError++-- | Exception type for hasql pool usage errors+data HasqlError = HasqlError Hasql.UsageError+ deriving (Show)++instance Exception HasqlError++-- | Provides the db connection and some IHP-specific db configuration+data ModelContext = ModelContext+ { hasqlPool :: Hasql.Pool -- ^ Hasql pool for prepared statement-based queries+ , transactionRunner :: Maybe TransactionRunner -- ^ When set, queries are sent through this runner instead of 'HasqlPool.use' directly+ -- | Logs all queries to this logger at log level info+ , logger :: Logger+ -- | A callback that is called whenever a specific table is accessed using a SELECT query+ , trackTableReadCallback :: Maybe (Text -> IO ())+ -- | Is set to a value if row level security was enabled at runtime+ , rowLevelSecurity :: Maybe RowLevelSecurityContext+ }++-- | When row level security is enabled at runtime, this keeps track of the current+-- logged in user and the postgresql role to switch to.+data RowLevelSecurityContext = RowLevelSecurityContext+ { rlsAuthenticatedRole :: Text -- ^ Default is @ihp_authenticated@. This value comes from the @IHP_RLS_AUTHENTICATED_ROLE@ env var.+ , rlsUserId :: Text -- ^ The user id of the current logged in user+ }++type family GetModelById id :: Type where+ GetModelById (Maybe (Id' tableName)) = Maybe (GetModelByTableName tableName)+ GetModelById (Id' tableName) = GetModelByTableName tableName++type family GetTableName model :: Symbol+type family GetModelByTableName (tableName :: Symbol) :: Type++-- | Provides the primary key type for a given table. The instances are usually declared+-- by the generated haskell code in Generated.Types+--+-- __Example:__ Defining the primary key for a users table+--+-- > type instance PrimaryKey "users" = UUID+--+--+-- __Example:__ Defining the primary key for a table with a SERIAL pk+--+-- > type instance PrimaryKey "projects" = Int+--+type family PrimaryKey (tableName :: Symbol)++type family GetModelName model :: Symbol++type family Include (name :: GHC.Types.Symbol) model++type family Include' (name :: [GHC.Types.Symbol]) model where+ Include' '[] model = model+ Include' (x:xs) model = Include' xs (Include x model)++-- | Helper type to deal with models where relations are included or that are only partially fetched+-- Examples:+--+-- >>> NormalizeModel (Include "author_id" Post)+-- Post+--+-- >>> NormalizeModel Post+-- Post+type NormalizeModel model = GetModelByTableName (GetTableName model)++newtype Id' table = Id (PrimaryKey table)++deriving instance (Eq (PrimaryKey table)) => Eq (Id' table)+deriving instance (Ord (PrimaryKey table)) => Ord (Id' table)+deriving instance (Hashable (PrimaryKey table)) => Hashable (Id' table)+deriving instance (KnownSymbol table, Data (PrimaryKey table)) => Data (Id' table)+deriving instance (KnownSymbol table, NFData (PrimaryKey table)) => NFData (Id' table)++-- | We need to map the model to its table name to prevent infinite recursion in the model data definition+-- E.g. `type Project = Project' { id :: Id Project }` will not work+-- But `type Project = Project' { id :: Id "projects" }` will+type Id model = Id' (GetTableName model)++type FieldName = ByteString++-- | The error message of a validator can be either a plain text value or a HTML formatted value+data Violation+ = TextViolation { message :: !Text } -- ^ Plain text validation error, like "cannot be empty"+ | HtmlViolation { message :: !Text } -- ^ HTML formatted, already pre-escaped validation error, like "Invalid, please <a href="http://example.com">check the documentation</a>"+ deriving (Eq, Show)++-- | Every IHP database record has a magic @meta@ field which keeps a @MetaBag@ inside. This data structure is used e.g. to keep track of the validation errors that happend.+data MetaBag = MetaBag+ { annotations :: ![(Text, Violation)] -- ^ Stores validation failures, as a list of (field name, error) pairs. E.g. @annotations = [ ("name", TextViolation "cannot be empty") ]@+ , touchedFields :: !Integer -- ^ Bitmask of touched fields. Whenever a 'set' is called on a field, its bit is set via '.|.'. Only touched fields are saved to the database when you call 'updateRecord'. For ≤64 columns, fits in a single machine word.+ , originalDatabaseRecord :: Maybe Dynamic -- ^ When the record has been fetched from the database, we save the initial database record here. This is used by 'didChange' to check if a field value is different from the initial database value.+ } deriving (Show)++-- | Maps a field name to its bit position in the 'touchedFields' bitmask.+-- Instances are generated by the schema compiler for each table's fields.+class FieldBit (name :: Symbol) model where+ fieldBit :: Integer++instance Eq MetaBag where+ MetaBag { annotations, touchedFields } == MetaBag { annotations = annotations', touchedFields = touchedFields' } = annotations == annotations' && touchedFields == touchedFields'+ {-# INLINE (==) #-}++-- | Represents fields that have a default value in an SQL schema+--+-- The 'Default' constructor represents the default value from the schema,+-- while the 'NonDefault' constructor holds some other value for the field+data FieldWithDefault valueType = Default | NonDefault valueType deriving (Eq, Show)++-- | Represents fields that may have been updated+--+-- The 'NoUpdate' constructor represents the existing value in the database,+-- while the 'Update' constructor holds some new value for the field+data FieldWithUpdate name value+ = NoUpdate (Proxy name)+ | Update value+ deriving (Eq, Show)++-- | Record type for objects of model types labeled with values from different database tables. (e.g. comments labeled with the IDs of the posts they belong to).+data LabeledData a b = LabeledData { labelValue :: a, contentValue :: b }+ deriving (Show)++-- | Thrown by 'fetchOne' when the query result is empty+data RecordNotFoundException+ = RecordNotFoundException { queryAndParams :: Text }+ deriving (Show)++instance Exception RecordNotFoundException++-- | Whenever calls to 'Database.PostgreSQL.Simple.query' or 'Database.PostgreSQL.Simple.execute'+-- raise an 'Database.PostgreSQL.Simple.SqlError' exception, we wrap that exception in this data structure.+-- This allows us to show the actual database query that has triggered the error.+data EnhancedSqlError+ = EnhancedSqlError+ { sqlErrorQuery :: Query+ , sqlErrorQueryParams :: Text+ , sqlError :: PG.SqlError+ } deriving (Show)++instance Exception EnhancedSqlError++-- | Extract the SQL error message as Text from an EnhancedSqlError.+--+-- This avoids downstream packages needing to import postgresql-simple+-- to access the 'sqlErrorMsg' field on 'PG.SqlError'.+enhancedSqlErrorMessage :: EnhancedSqlError -> Text+enhancedSqlErrorMessage e = Data.Text.Encoding.decodeUtf8 e.sqlError.sqlErrorMsg+{-# INLINE enhancedSqlErrorMessage #-}++class CanCreate a where+ create :: (?modelContext :: ModelContext) => a -> IO a+ createMany :: (?modelContext :: ModelContext) => [a] -> IO [a]++ -- | Like 'createRecord' but doesn't return the created record+ createRecordDiscardResult :: (?modelContext :: ModelContext) => a -> IO ()+ createRecordDiscardResult record = do+ _ <- create record+ pure ()++class CanUpdate a where+ updateRecord :: (?modelContext :: ModelContext) => a -> IO a++ -- | Like 'updateRecord' but doesn't return the updated record+ updateRecordDiscardResult :: (?modelContext :: ModelContext) => a -> IO ()+ updateRecordDiscardResult record = do+ _ <- updateRecord record+ pure ()++class ParsePrimaryKey primaryKey where+ parsePrimaryKey :: Text -> Maybe primaryKey
@@ -25,7 +25,6 @@ import IHP.HaskellSupport import Data.Text (Text) import Data.String.Conversions (cs)-import qualified Data.Char as Char import qualified Text.Inflections as Inflector import qualified Data.Maybe as Maybe import qualified Data.List as List@@ -33,6 +32,7 @@ import qualified Text.Inflections import qualified Data.Text as Text import Text.Countable (singularize, pluralize)+import qualified Data.Text.Slugger -- | Transforms a underscore table name to a camel case model name. --@@ -245,17 +245,7 @@ -- >>> toSlug "Hallo! @ Welt" -- "hallo-welt" toSlug :: Text -> Text-toSlug text =- text- |> Text.toLower- |> Text.map replaceChar- |> Text.words- |> Text.intercalate "-"- where- replaceChar 'ä' = 'a'- replaceChar 'ö' = 'o'- replaceChar 'ü' = 'u'- replaceChar char = if Char.isAlphaNum char && Char.isAscii char then char else ' '+toSlug text = Data.Text.Slugger.toSlug text -- | Transform a data-field name like @userName@ to a friendly human-readable name like @User name@
@@ -1,274 +0,0 @@-{-|-Module: IHP.PGListener-Description: Event listener handling pg_notify messages-Copyright: (c) digitally induced GmbH, 2021--This module is solving the problem, that previously IHP was using one database connection-per running @LISTEN ..;@ statement. A @PGListener@ provides one central object to listen on-postgres channels, without manually dealing with connection management.--}-module IHP.PGListener-( Channel-, Callback-, Subscription (..)-, PGListener (..)-, init-, stop-, withPGListener-, subscribe-, subscribeJSON-, unsubscribe-) where--import IHP.Prelude hiding (init)-import IHP.ModelSupport-import qualified Data.Set as Set-import qualified Database.PostgreSQL.Simple as PG-import qualified Database.PostgreSQL.Simple.Types as PG-import qualified Database.PostgreSQL.Simple.Notification as PG-import qualified Data.UUID.V4 as UUID-import Control.Concurrent.MVar (MVar)-import qualified Control.Concurrent.MVar as MVar-import Data.HashMap.Strict as HashMap-import qualified Control.Concurrent.Async as Async-import qualified Data.List as List-import qualified Data.Aeson as Aeson-import qualified IHP.Log as Log-import qualified Control.Exception.Safe as Exception-import qualified Control.Concurrent.Chan.Unagi as Queue-import qualified Control.Concurrent---- TODO: How to deal with timeout of the connection?---- | The channel is like the event name------ It's used in the postgres NOTIFY call:------ > NOTIFY channel [ , payload ]----type Channel = ByteString---- | An event callback receives the postgres notification object and can do IO-type Callback = PG.Notification -> IO ()---- | Returned by a call to 'subscribe'-data Subscription = Subscription- { id :: !UUID- , reader :: !(Async ())- , inChan :: !(Queue.InChan PG.Notification)- , channel :: !Channel- }---- | The main datatype of the service. Keeps tracks of all channels we're watching on, as well as all open subscriptions------ Use 'init' to create a new object and 'stop' to deallocate it.-data PGListener = PGListener- { modelContext :: !ModelContext- , listeningTo :: !(MVar (Set Channel))- , listenTo :: !(MVar Channel)- , subscriptions :: !(IORef (HashMap Channel [Subscription]))- , notifyLoopAsync :: !(Async ())- }---- | Creates a new 'PGListener' object------ > let modelContext = ..--- > pgListener <- PGListener.init modelContext------ This will start a new async listening for postgres notifications. This will take one connection--- from the database pool and keep it blocked until 'stop' is called.----init :: ModelContext -> IO PGListener-init modelContext = do- listeningTo <- MVar.newMVar Set.empty- subscriptions <- newIORef HashMap.empty- listenTo <- MVar.newEmptyMVar-- let ?modelContext = modelContext- notifyLoopAsync <- async (notifyLoop listeningTo listenTo subscriptions)- pure PGListener { modelContext, listeningTo, subscriptions, listenTo, notifyLoopAsync }---- | Stops the database listener async and puts the database connection used back into the database pool------ > PGListener.stop pgListener----stop :: PGListener -> IO ()-stop PGListener { notifyLoopAsync } = do- cancel notifyLoopAsync--withPGListener :: ModelContext -> (PGListener -> IO a) -> IO a-withPGListener modelContext =- Exception.bracket (init modelContext) stop---- | After you subscribed to a channel, the provided callback will be called whenever there's a new--- notification on the channel.------ > pgListener <- PGListener.init--- >--- > let callback notification = do--- > let payload :: Text = cs (notification.notificationData)--- > putStrLn ("Received notification: " <> payload)--- > --- > subscription <- pgListener |> PGListener.subscribe "my_channel" callback------ The @callback@ function will now be called whenever @NOTIFY "my_channel", "my payload"@ is executed on the postgres server.------ When the subscription is not used anymore, call 'unsubscribe' to stop the callback from being called anymore.----subscribe :: Channel -> Callback -> PGListener -> IO Subscription-subscribe channel callback pgListener = do- id <- UUID.nextRandom- listenToChannelIfNeeded channel pgListener-- -- We use a queue here to guarantee that the messages are processed in the right order- -- while keeping high performance.- --- -- A naive implementation might be just kicking of an async for each message. But in that case- -- the messages might be delivered to the final consumer out of order.- (inChan, outChan) <- Queue.newChan- - let- -- We need to log any exception, otherwise there might be silent errors- logException :: SomeException -> IO ()- logException exception = logError pgListener ("Error in pg_notify handler: " <> cs (displayException exception))-- reader <- async $ forever do- message <- Queue.readChan outChan- callback message `Exception.catch` logException- let subscription = Subscription { .. }-- modifyIORef' (pgListener.subscriptions) (HashMap.insertWith mappend channel [subscription] )-- pure subscription---- | Like 'subscribe' but decodes the notification payload from JSON and passes the decoded data structure to the callback------ When JSON parsing fails, this will ignore the notification.------ > pgListener <- PGListener.init--- >--- > let callback (jsonObject :: Aeson.Value) = do--- > putStrLn ("Received notification: " <> tshow jsonObject)--- > --- > subscription <- pgListener |> PGListener.subscribeJSON "my_json_channel" callback------ The @callback@ function will now be called whenever @NOTIFY "my_json_channel", "{\"hello\":\"world\"}"@ is executed on the postgres server.-subscribeJSON :: Aeson.FromJSON jsonValue => Channel -> (jsonValue -> IO ()) -> PGListener -> IO Subscription-subscribeJSON channel callback pgListener = subscribe channel callback' pgListener- where- callback' notification = do- let payload = (notification.notificationData)- case Aeson.decodeStrict' payload of- Just payload -> callback payload- Nothing -> logError pgListener ("PGListener.subscribeJSON: Failed to parse " <> tshow payload)---- | Stops the callback of a subscription from receiving further notifications------ > pgListener <- PGListener.init--- >--- > subscription <- pgListener |> PGListener.subscribe "my_channel" callback--- > doSomethingExpensive--- > pgListener |> PGListener.unsubscribe subscription----unsubscribe :: Subscription -> PGListener -> IO ()-unsubscribe subscription@(Subscription { .. }) pgListener = do- let- deleteById :: [Subscription] -> [Subscription]- deleteById = List.deleteBy (\a b -> a.id == b.id) subscription- modifyIORef' (pgListener.subscriptions) (HashMap.adjust deleteById channel)- uninterruptibleCancel reader- pure () ---- | Runs a @LISTEN ..;@ statements on the postgres connection, if not already listening on that channel-listenToChannelIfNeeded :: Channel -> PGListener -> IO ()-listenToChannelIfNeeded channel pgListener = do- listeningTo <- MVar.readMVar (pgListener.listeningTo)- let alreadyListening = channel `Set.member` listeningTo-- unless alreadyListening do- MVar.putMVar (pgListener.listenTo) channel- -- ---- | The main loop that is receiving events from the database and triggering callbacks------ Todo: What happens when the connection dies?-notifyLoop :: (?modelContext :: ModelContext) => MVar (Set Channel) -> MVar Channel -> IORef (HashMap Channel [Subscription]) -> IO ()-notifyLoop listeningToVar listenToVar subscriptions = do- -- Wait until the first LISTEN is requested before taking a database connection from the pool- MVar.readMVar listenToVar-- let innerLoop = do- withDatabaseConnection \databaseConnection -> do-- -- If listeningTo already contains channels, this means that previously the database connection- -- died, so we're restarting here. Therefore we need to replay all LISTEN calls to restore the previous state- listeningTo <- MVar.readMVar listeningToVar- forEach listeningTo (listenToChannel databaseConnection)-- -- This loop reads channels from the 'listenToVar' and then triggers a LISTEN statement on- -- the current database connections- let listenLoop = forever do- channel <- MVar.takeMVar listenToVar- - MVar.modifyMVar_ listeningToVar \listeningTo -> do- let alreadyListening = channel `Set.member` listeningTo-- if alreadyListening- then pure listeningTo- else do- listenToChannel databaseConnection channel- pure (Set.insert channel listeningTo)-- Async.withAsync listenLoop \listenLoopAsync -> do- forever do- notification <- PG.getNotification databaseConnection- let channel = notification.notificationChannel-- allSubscriptions <- readIORef subscriptions- let channelSubscriptions = allSubscriptions- |> HashMap.lookup channel- |> fromMaybe []-- forEach channelSubscriptions \subscription -> do- let inChan = subscription.inChan- Queue.writeChan inChan notification-- -- Initial delay (in microseconds)- let initialDelay = 500 * 1000- -- Max delay (in microseconds)- let maxDelay = 60 * 1000 * 1000- -- This outer loop restarts the listeners if the database connection dies (e.g. due to a timeout)- let retryLoop delay isFirstError = do- result <- Exception.tryAny innerLoop- case result of- Left error -> do- let ?context = ?modelContext -- Log onto the modelContext logger- if isFirstError then do- Log.info ("PGListener is going to restart, loop failed with exception: " <> (displayException error) <> ". Retrying immediately.")- retryLoop delay False -- Retry with no delay interval on first error, but will increase delay interval in subsequent retries - else do- let increasedDelay = delay * 2 -- Double current delay- let nextDelay = min increasedDelay maxDelay -- Picks whichever delay is lowest of increasedDelay * 2 or maxDelay- Log.info ("PGListener is going to restart, loop failed with exception: " <> (displayException error) <> ". Retrying in " <> cs (printTimeToNextRetry delay) <> ".")- Control.Concurrent.threadDelay delay -- Sleep for the current delay- retryLoop nextDelay False -- Retry with longer interval- Right _ -> - retryLoop initialDelay True -- If all went well, re-run with no sleeping and reset current delay to the initial value- retryLoop initialDelay True--printTimeToNextRetry :: Int -> Text-printTimeToNextRetry microseconds- | microseconds >= 1000000000 = show (microseconds `div` 1000000000) <> " min"- | microseconds >= 1000000 = show (microseconds `div` 1000000) <> " s"- | microseconds >= 1000 = show (microseconds `div` 1000) <> " ms"- | otherwise = show microseconds <> " µs"--listenToChannel :: PG.Connection -> Channel -> IO ()-listenToChannel databaseConnection channel = do- PG.execute databaseConnection "LISTEN ?" [PG.Identifier (cs channel)]- pure ()--logError :: PGListener -> Text -> IO ()-logError pgListener message = let ?context = pgListener.modelContext in Log.error message
@@ -0,0 +1,66 @@+{-# LANGUAGE AllowAmbiguousTypes, UndecidableInstances, FlexibleInstances, IncoherentInstances, PolyKinds, ScopedTypeVariables #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | Consolidates all postgresql-simple FromField\/ToField\/FromRow orphan instances+-- for IHP model types.+--+-- Note: JobStatus FromField\/ToField instances remain in "IHP.Job.Queue" to+-- avoid a circular dependency through @IHP.Job.Types -> IHP.Prelude -> IHP.ModelSupport@.+module IHP.PGSimpleCompat () where++import Prelude+import Data.String (IsString(..))+import Data.String.Conversions (cs)+import Data.Data (Typeable)+import GHC.TypeLits (KnownSymbol, symbolVal)+import Database.PostgreSQL.Simple.FromField (FromField(..))+import Database.PostgreSQL.Simple.ToField (ToField(..), Action(..))+import qualified Database.PostgreSQL.Simple.FromRow as PGFR+import qualified Database.PostgreSQL.Simple.Types as PG+import IHP.ModelSupport.Types (Id'(..), PrimaryKey, LabeledData(..), FieldWithDefault(..), FieldWithUpdate(..))+import IHP.NameSupport (fieldNameToColumnName)++-- Import postgresql-simple-postgresql-types for FromField/ToField instances+-- of all postgresql-types types (Point, Polygon, Inet, Interval, etc.)+import Database.PostgreSQL.Simple.PostgresqlTypes ()++-- Id instances++instance FromField (PrimaryKey model) => FromField (Id' model) where+ {-# INLINE fromField #-}+ fromField value metaData = do+ fieldValue <- fromField value metaData+ pure (Id fieldValue)++instance ToField (PrimaryKey model) => ToField (Id' model) where+ {-# INLINE toField #-}+ toField (Id pk) = toField pk++instance (ToField (Id' a), ToField (Id' b)) => ToField (Id' a, Id' b) where+ {-# INLINE toField #-}+ toField (a, b) = Many [Plain "(", toField a, Plain ",", toField b, Plain ")"]++-- LabeledData instance++instance (FromField label, PGFR.FromRow a) => PGFR.FromRow (LabeledData label a) where+ fromRow = LabeledData <$> PGFR.field <*> PGFR.fromRow++-- FieldWithDefault / FieldWithUpdate instances++instance ToField valueType => ToField (FieldWithDefault valueType) where+ toField Default = Plain "DEFAULT"+ toField (NonDefault a) = toField a++instance (KnownSymbol name, ToField value) => ToField (FieldWithUpdate name value) where+ toField (NoUpdate name) =+ Plain (fromString $ cs $ fieldNameToColumnName $ cs $ symbolVal name)+ toField (Update a) = toField a++-- List instances (PGArray wrappers)++instance ToField value => ToField [value] where+ toField list = toField (PG.PGArray list)++instance (FromField value, Typeable value) => FromField [value] where+ fromField field value = PG.fromPGArray <$> (fromField field value)+
@@ -0,0 +1,14 @@+module IHP.PGVersion (defaultUuidFunction) where++import IHP.Prelude+import qualified System.Environment as Env+import Text.Read (readMaybe)++-- | Returns the default UUID function based on the @IHP_POSTGRES_VERSION@ env var.+-- When set to 18 or higher, returns @"uuidv7"@ (PostgreSQL 18+ native function).+-- Otherwise returns @"uuid_generate_v4"@ (requires uuid-ossp extension).+defaultUuidFunction :: IO Text+defaultUuidFunction = do+ pgVersion <- fromMaybe "17" <$> Env.lookupEnv "IHP_POSTGRES_VERSION"+ let version = fromMaybe 17 (readMaybe pgVersion :: Maybe Int)+ pure if version >= 18 then "uuidv7" else "uuid_generate_v4"
@@ -1,57 +0,0 @@-{-|-Module: IHP.PageHead.ControllerFunctions-Description: Manage the @<title>@ and @<meta>@ tags of your HTML pages-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.PageHead.ControllerFunctions-( setTitle-, setDescription-, setOGTitle-, setOGType-, setOGDescription-, setOGUrl-, setOGImage-) where--import IHP.Prelude-import IHP.PageHead.Types-import IHP.Controller.Context---- | Sets the page title. Can be accessed using '{pageTitle}' inside your @Layout.hs@.------ Example:------ > action ShowProjectAction { projectId } = do--- > project <- fetch projectId--- > setTitle (project.title)--- >------ Inside your layout use it like:------ > defaultLayout :: Html -> Html--- > defaultLayout inner = [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > </head>--- > |]----setTitle :: (?context :: ControllerContext) => Text -> IO ()-setTitle title = putContext (PageTitle title)--setDescription :: (?context :: ControllerContext) => Text -> IO ()-setDescription description = putContext (PageDescription description)--setOGTitle :: (?context :: ControllerContext) => Text -> IO ()-setOGTitle title = putContext (OGTitle title)--setOGType :: (?context :: ControllerContext) => Text -> IO ()-setOGType type_ = putContext (OGType type_)--setOGDescription :: (?context :: ControllerContext) => Text -> IO ()-setOGDescription description = putContext (OGDescription description)--setOGUrl :: (?context :: ControllerContext) => Text -> IO ()-setOGUrl url = putContext (OGUrl url)--setOGImage :: (?context :: ControllerContext) => Text -> IO ()-setOGImage image = putContext (OGImage image)
@@ -1,21 +0,0 @@-{-|-Module: IHP.PageHead.Types-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.PageHead.Types where--import IHP.Prelude--newtype PageTitle = PageTitle Text--newtype PageDescription = PageDescription Text--newtype OGTitle = OGTitle Text--newtype OGType = OGType Text--newtype OGDescription = OGDescription Text--newtype OGUrl = OGUrl Text--newtype OGImage = OGImage Text
@@ -1,261 +0,0 @@-{-|-Module: IHP.PageHead.ViewFunctions-Description: Manage the @<title>@ and @<meta>@ tags of your HTML pages-Copyright: (c) digitally induced GmbH, 2021--}-module IHP.PageHead.ViewFunctions-( pageTitle-, pageTitleOrDefault-, pageTitleOrNothing-, descriptionOrDefault-, ogTitleOrDefault-, ogTypeOrDefault-, ogDescriptionOrDefault-, ogUrl-, ogImage-, module IHP.PageHead.ControllerFunctions -- | Re-export as we want to call setTitle from the beforeRender hook-) where--import IHP.Prelude-import IHP.PageHead.Types-import IHP.Controller.Context-import IHP.PageHead.ControllerFunctions-import IHP.HSX.QQ (hsx)-import Text.Blaze.Html5 (Html)---- | Returns the current page title. The title can be set using @setTitle "my title"@ from the action.------ If the title hasn't been set yet, this will return an empty string. You can also use 'pageTitleOrDefault' to pass a custom default title.------ You can use this inside your @<title>@ tag like this:------ > [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > </head>--- > |]--------- *App-wide default title:*------ You can set a app-wide default title by calling 'setTitle' from the @FrontController.hs@:------ > instance InitControllerContext Web where--- > initContext = do--- > setLayout defaultLayout--- > initAutoRefresh--- > setTitle "Jobs"--------- *View-specific title:*--- --- You can set a custom title inside the view by calling 'setTitle' inside the 'beforeRender' hook.------ > module JobSite.View.JobPositions.Show where--- > --- > instance View ShowView where--- > beforeRender ShowView { .. } = do--- > setTitle "Custom title"--- > --- > html ShowView { .. } = [hsx|..|]-pageTitle :: (?context :: ControllerContext) => Text-pageTitle = pageTitleOrDefault ""---- | Returns the current page title, like 'pageTitle' but returns a provided default value instead of an empty string if no title is set.------ You can use this inside your @<title>@ tag like this:------ > [hsx|--- > <head>--- > <title>{pageTitleOrDefault "My Application"}</title>--- > </head>--- > |]-pageTitleOrDefault :: (?context :: ControllerContext) => Text -> Text-pageTitleOrDefault defaultValue = pageTitleOrNothing |> fromMaybe defaultValue---- | Returns the current page title or Nothing if not set yet-pageTitleOrNothing :: (?context :: ControllerContext) => Maybe Text-pageTitleOrNothing = case maybeFromFrozenContext @PageTitle of- Just (PageTitle title) -> Just title- Nothing -> Nothing----- | Returns the meta og:title element. The og:title can be set using @setOGTitle "my title"@ from the view.------ You can use this inside your Layout like this:------ > [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > {ogTitleOrDefault "default title"}--- > </head>--- > |]--------- *View-specific og:title:*--- --- You can override the default og:title inside the view by calling 'setOGTitle' inside the 'beforeRender' hook:------ > module JobSite.View.JobPositions.Show where--- > --- > instance View ShowView where--- > beforeRender ShowView { .. } = do--- > setOGTitle "Custom title"--- > --- > html ShowView { .. } = [hsx|..|]-ogTitleOrDefault :: (?context :: ControllerContext) => Text -> Html-ogTitleOrDefault defaultValue = [hsx|<meta property="og:title" content={content}/>|]- where- content = case maybeFromFrozenContext @OGTitle of- Just (OGTitle title) -> title- Nothing -> defaultValue---- | Returns @<meta name="description" content="Lorem Ipsum">@ element. The description can be set using @setDescription "my description"@ from the view.------ You can use this inside your Layout like this:------ > [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > {descriptionOrDefault "CO2 Database"}--- > </head>--- > |]--------- *View-specific description:*--- --- You can override the default description inside the view by calling 'setDescription' inside the 'beforeRender' hook:------ > module JobSite.View.JobPositions.Show where--- > --- > instance View ShowView where--- > beforeRender ShowView { .. } = do--- > setDescription "The CO2 Footprint of beef is about 67kg CO2 per 1kg of beef."--- > --- > html ShowView { .. } = [hsx|..|]-descriptionOrDefault :: (?context :: ControllerContext) => Text -> Html-descriptionOrDefault defaultValue = [hsx|<meta name="description" content={content}/>|]- where- content = case maybeFromFrozenContext @PageDescription of- Just (PageDescription description) -> description- Nothing -> defaultValue---- | Returns the meta og:type element. The og:type can be set using @setOGType "data"@ from the view.------ You can use this inside your Layout like this:------ > [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > {ogTypeOrDefault "data"}--- > </head>--- > |]--------- *View-specific og:type:*--- --- You can override the default og:type inside the view by calling 'setOGType' inside the 'beforeRender' hook:------ > module JobSite.View.JobPositions.Show where--- > --- > instance View ShowView where--- > beforeRender ShowView { .. } = do--- > setOGType "mytype"--- > --- > html ShowView { .. } = [hsx|..|]-ogTypeOrDefault :: (?context :: ControllerContext) => Text -> Html-ogTypeOrDefault defaultValue = [hsx|<meta property="og:type" content={content}/>|]- where- content = case maybeFromFrozenContext @OGType of- Just (OGType type_) -> type_- Nothing -> defaultValue---- | Returns the meta og:description element. The og:description can be set using @setOGDescription "my description"@ from the view.------ You can use this inside your Layout like this:------ > [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > {ogDescriptionOrDefault "CO2 Database"}--- > </head>--- > |]--------- *View-specific og:description:*--- --- You can override the default og:description inside the view by calling 'setOGDescription' inside the 'beforeRender' hook:------ > module JobSite.View.JobPositions.Show where--- > --- > instance View ShowView where--- > beforeRender ShowView { .. } = do--- > setOGDescription "The CO2 Footprint of beef is about 67kg CO2 per 1kg of beef."--- > --- > html ShowView { .. } = [hsx|..|]-ogDescriptionOrDefault :: (?context :: ControllerContext) => Text -> Html-ogDescriptionOrDefault defaultValue = [hsx|<meta property="og:description" content={content}/>|]- where- content = case maybeFromFrozenContext @OGDescription of- Just (OGDescription description) -> description- Nothing -> defaultValue----- | Returns the meta og:url element if @setOGUrl "https://example.com/"@ was called before.------ You can use this inside your Layout like this:------ > [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > {ogUrl}--- > </head>--- > |]------ When 'setOGUrl' is not called, no meta tag will be rendered.------ *Setting og:url:*--- --- You can set the og:url inside the view by calling 'setOGUrl' inside the 'beforeRender' hook:------ > module JobSite.View.JobPositions.Show where--- > --- > instance View ShowView where--- > beforeRender ShowView { .. } = do--- > setOGUrl (urlTo ShowAction { .. })--- > --- > html ShowView { .. } = [hsx|..|]-ogUrl :: (?context :: ControllerContext) => Html-ogUrl = case maybeFromFrozenContext @OGUrl of- Just (OGUrl url) -> [hsx|<meta property="og:url" content={url}/>|]- Nothing -> mempty----- | Returns the meta og:image element if @setOGImage "https://example.com/image.png"@ was called before.------ You can use this inside your Layout like this:------ > [hsx|--- > <head>--- > <title>{pageTitle}</title>--- > {ogImage}--- > </head>--- > |]------ When 'setOGImage' is not called, no meta tag will be rendered.------ *Setting og:image:*--- --- You can set the og:image inside the view by calling 'setOGImage' inside the 'beforeRender' hook:------ > module JobSite.View.JobPositions.Show where--- > --- > instance View ShowView where--- > beforeRender ShowView { .. } = do--- > setOGImage "https://example.com/image.png"--- > --- > html ShowView { .. } = [hsx|..|]-ogImage :: (?context :: ControllerContext) => Html-ogImage = case maybeFromFrozenContext @OGImage of- Just (OGImage url) -> [hsx|<meta property="og:image" content={url}/>|]- Nothing -> mempty
@@ -18,9 +18,13 @@ import IHP.Pagination.Types ( Options(..), Pagination(..) ) import IHP.QueryBuilder ( HasQueryBuilder, filterWhereILike, limit, offset ) import IHP.Fetch (fetchCount)-import IHP.ModelSupport (GetModelByTableName, sqlQuery, sqlQueryScalar, Table)--import Database.PostgreSQL.Simple (FromRow, ToRow, Query(..), Only(Only), (:.)(..))+import IHP.ModelSupport (GetModelByTableName, sqlQueryHasql, Table)+import IHP.Hasql.FromRow (FromRowHasql(..))+import IHP.Hasql.Encoders (ToSnippetParams(..), sqlToSnippet)+import Network.Wai (Request)+import qualified Hasql.Decoders as Decoders+import qualified Hasql.DynamicStatements.Snippet as Snippet+import Data.Text.Encoding (encodeUtf8) -- | Paginate a query, with the following default options: --@@ -49,6 +53,7 @@ (?context::ControllerContext , ?modelContext :: ModelContext , ?theAction :: controller+ , ?request :: Request , KnownSymbol table , HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table@@ -80,6 +85,7 @@ (?context::ControllerContext , ?modelContext :: ModelContext , ?theAction :: controller+ , ?request :: Request , KnownSymbol table , HasQueryBuilder queryBuilderProvider joinRegister) => Options@@ -121,6 +127,7 @@ -- > render IndexView { .. } filterList :: forall name table model queryBuilderProvider joinRegister . (?context::ControllerContext+ , ?request :: Request , KnownSymbol name , HasField name model Text , model ~ GetModelByTableName table@@ -173,12 +180,13 @@ -- -- *AutoRefresh:* When using 'paginatedSqlQuery' with AutoRefresh, you need to use 'trackTableRead' to let AutoRefresh know that you have accessed a certain table. Otherwise AutoRefresh will not watch table of your custom sql query. paginatedSqlQuery- :: ( FromRow model- , ToRow parameters+ :: ( FromRowHasql model+ , ToSnippetParams parameters , ?context :: ControllerContext , ?modelContext :: ModelContext+ , ?request :: Request )- => Query -> parameters -> IO ([model], Pagination)+ => Text -> parameters -> IO ([model], Pagination) paginatedSqlQuery = paginatedSqlQueryWithOptions defaultPaginationOptions -- | Runs a raw sql query and adds pagination to it.@@ -196,15 +204,20 @@ -- -- *AutoRefresh:* When using 'paginatedSqlQuery' with AutoRefresh, you need to use 'trackTableRead' to let AutoRefresh know that you have accessed a certain table. Otherwise AutoRefresh will not watch table of your custom sql query. paginatedSqlQueryWithOptions- :: ( FromRow model- , ToRow parameters+ :: ( FromRowHasql model+ , ToSnippetParams parameters , ?context :: ControllerContext , ?modelContext :: ModelContext+ , ?request :: Request )- => Options -> Query -> parameters -> IO ([model], Pagination)+ => Options -> Text -> parameters -> IO ([model], Pagination) paginatedSqlQueryWithOptions options sql placeholders = do- count :: Int <- sqlQueryScalar ("SELECT count(subquery.*) FROM (" <> sql <> ") as subquery") placeholders+ let pool = ?modelContext.hasqlPool+ let baseParams = toSnippetParams placeholders + let countSnippet = sqlToSnippet ("SELECT count(subquery.*) FROM (" <> encodeUtf8 sql <> ") as subquery") baseParams+ count :: Int <- sqlQueryHasql pool countSnippet (Decoders.singleRow (Decoders.column (Decoders.nonNullable (fromIntegral <$> Decoders.int8))))+ let pageSize = pageSize' options pagination = Pagination { pageSize = pageSize@@ -213,20 +226,19 @@ , window = windowSize options } - results :: [model] <- sqlQuery- ("SELECT subquery.* FROM (" <> sql <> ") as subquery LIMIT ? OFFSET ?")- (placeholders :. Only pageSize :. Only (offset' pageSize page))+ let resultsSnippet = sqlToSnippet ("SELECT subquery.* FROM (" <> encodeUtf8 sql <> ") as subquery LIMIT ? OFFSET ?") (baseParams <> [Snippet.param pageSize, Snippet.param (offset' pageSize page)])+ results :: [model] <- sqlQueryHasql pool resultsSnippet (Decoders.rowList hasqlRowDecoder) pure (results, pagination) -- We limit the page size to a maximum of 200, to prevent users from -- passing in query params with a value that could overload the -- database (e.g. maxItems=100000)-pageSize' :: (?context::ControllerContext) => Options -> Int+pageSize' :: (?request :: Request) => Options -> Int pageSize' options = min (max 1 $ paramOrDefault @Int (maxItems options) "maxItems") 200 -- Page and page size shouldn't be lower than 1.-page :: (?context::ControllerContext) => Int+page :: (?request :: Request) => Int page = max 1 $ paramOrDefault @Int 1 "page" offset' :: Int -> Int -> Int
@@ -1,6 +1,6 @@ module IHP.Pagination.Helpers where -import IHP.Prelude+import Prelude import IHP.Pagination.Types -- | Gets the number of the last page from a 'Pagination' state. Uses the total
@@ -1,6 +1,7 @@ module IHP.Pagination.Types where -import IHP.Prelude+import Prelude+import IHP.HaskellSupport (SetField(..)) data Pagination = Pagination
@@ -15,7 +15,7 @@ import IHP.Controller.Param (paramOrNothing) -import qualified Network.Wai as Wai+import Network.Wai import qualified Network.HTTP.Types.URI as Query import IHP.ViewSupport (theRequest, theCSSFramework) import qualified Data.Containers.ListUtils as List@@ -25,7 +25,7 @@ -- | Render a navigation for your pagination. This is to be used in your view whenever -- to allow users to change pages, including "Next" and "Previous". -- If there is only one page, this will not render anything.-renderPagination :: (?context::ControllerContext) => Pagination -> Html+renderPagination :: (?context :: ControllerContext, ?request :: Request) => Pagination -> Html renderPagination pagination@Pagination {currentPage, window, pageSize} = when (showPagination pagination) $ styledPagination theCSSFramework theCSSFramework paginationView where@@ -60,8 +60,8 @@ pageUrl n = path <> Query.renderQuery True newQueryString where -- "?page=" ++ show n ++ maybeFilter ++ maybeMaxItems- path = Wai.rawPathInfo theRequest- queryString = Wai.queryString theRequest+ path = theRequest.rawPathInfo+ queryString = theRequest.queryString newQueryString = queryString |> setQueryValue "page" (cs $ show n) |> maybeFilter@@ -69,8 +69,8 @@ itemsPerPageUrl n = path <> Query.renderQuery True newQueryString where- path = Wai.rawPathInfo theRequest- queryString = Wai.queryString theRequest+ path = theRequest.rawPathInfo+ queryString = theRequest.queryString newQueryString = queryString |> setQueryValue "maxItems" (cs $ tshow n) -- If we change the number of items, we should jump back to the first page@@ -129,27 +129,27 @@ -- <div class="container"> -- <div class="row justify-content-between"> -- <div class="col-7">--- <h1>Users<a href={pathTo NewUserAction} class="btn btn-primary ml-4">+ New</a></h1>+-- <h1>Users<a href={pathTo NewUserAction} class="btn btn-primary ms-4">+ New</a></h1> -- </div> -- <div class="col-5"> -- {renderFilter "Username"} -- </div> -- </div> -- </div>-renderFilter :: (?context::ControllerContext) =>+renderFilter :: (?context::ControllerContext, ?request :: Request) => Text -- ^ Placeholder text for the text box -> Html renderFilter placeholder = [hsx|- <form method="GET" action="" class="mt-2 float-right">- <div class="form-row">+ <form method="GET" action="" class="mt-2 float-end">+ <div class="row"> <div class="col-auto">- <label class="sr-only" for="inlineFormInput">Name</label>+ <label class="visually-hidden" for="inlineFormInput">Name</label> <input type="hidden" name="page" value="1"/> <input name="filter" type="text" class="form-control mb-2" id="inlineFormInput" placeholder={placeholder} value={boxValue}> </div> <div class="col-auto">- <button type="submit" class="btn btn-primary mb-2 mr-2">Filter</button>+ <button type="submit" class="btn btn-primary mb-2 me-2">Filter</button> <a class="btn btn-primary mb-2" href={clearFilterUrl}>Clear</a> </div> </div>@@ -159,8 +159,8 @@ boxValue = fromMaybe "" (paramOrNothing "filter") :: Text clearFilterUrl = path <> Query.renderQuery True newQueryString where- path = Wai.rawPathInfo theRequest- queryString = Wai.queryString theRequest+ path = theRequest.rawPathInfo+ queryString = theRequest.queryString newQueryString = queryString |> removeQueryItem "filter"
@@ -1,3 +1,8 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_HADDOCK not-home, hide #-}+#if __GLASGOW_HASKELL__ >= 912+{-# LANGUAGE NamedDefaults #-}+#endif module IHP.Prelude ( module CorePrelude , module Data.Text.IO@@ -32,7 +37,6 @@ , module IHP.NameSupport , module IHP.ModelSupport , module Data.TMap-, module Database.PostgreSQL.Simple , module Data.IORef , module Data.Time.Format , null@@ -42,6 +46,13 @@ , module NeatInterpolation , module GHC.Stack , module Data.Kind+, type (~)+, OsPath+, textToOsPath+, osPathToText+#if __GLASGOW_HASKELL__ >= 912+, default IsString+#endif ) where @@ -67,9 +78,8 @@ import Data.Data (Data) import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) import IHP.NameSupport-import IHP.ModelSupport (ModelContext (..), CanUpdate, NormalizeModel, Id, GetTableName, GetModelName, updateRecord, updateRecordDiscardResult, createRecord, deleteRecord, MetaBag (..))+import IHP.ModelSupport (ModelContext (..), CanUpdate, NormalizeModel, Id, GetTableName, GetModelName, updateRecord, updateRecordDiscardResult, createRecord, deleteRecord, MetaBag (..), FieldBit (..)) import Data.TMap (TMap)-import Database.PostgreSQL.Simple (FromRow) import Data.IORef import Data.Time.Format import Control.Exception.Safe (throw, throwIO, catch)@@ -78,6 +88,10 @@ import NeatInterpolation (trimming) import GHC.Stack (HasCallStack, CallStack) import Data.Kind (Type)+import Data.Type.Equality (type (~))+import System.OsPath (OsPath)+import qualified System.OsPath as OsPath+import System.IO.Unsafe (unsafePerformIO) -- Alias for haskell newcomers :) a ++ b = a <> b@@ -120,3 +134,23 @@ initMay = init plain = Data.String.Interpolate.i++-- | Pure conversion from Text to OsPath using UTF-8 encoding.+-- On POSIX (where IHP runs), this is safe for all valid Text values.+textToOsPath :: Text -> OsPath+textToOsPath text = unsafePerformIO (OsPath.encodeUtf (cs text))+{-# NOINLINE textToOsPath #-}++-- | Pure conversion from OsPath to Text using UTF-8 decoding.+osPathToText :: OsPath -> Text+osPathToText path = cs (unsafePerformIO (OsPath.decodeUtf path))+{-# NOINLINE osPathToText #-}++-- | Allows using string literals as OsPath values with OverloadedStrings.+instance IsString OsPath where+ fromString s = unsafePerformIO (OsPath.encodeUtf s)+ {-# NOINLINE fromString #-}++#if __GLASGOW_HASKELL__ >= 912+default IsString (Text, String)+#endif
@@ -1,1192 +1,108 @@-{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}-{-|-Module: IHP.QueryBuilder-Description: Tool to build simple sql queries-Copyright: (c) digitally induced GmbH, 2020-QueryBuilder is mainly used for doing simple `SELECT` sql queries. It allows dynamic-creation of sql queries in a type safe way.--For more complex sql queries, use 'IHP.ModelSupport.sqlQuery'.--}-module IHP.QueryBuilder-( query-, QueryBuilder (..)-, In (In)-, orderBy-, orderByJoinedTable-, orderByAsc-, orderByAscJoinedTable-, orderByDesc-, orderByDescJoinedTable-, limit-, offset-, queryUnion-, queryUnionList-, queryOr-, DefaultScope (..)-, filterWhere-, filterWhereCaseInsensitive-, filterWhereNot-, filterWhereIn-, filterWhereInCaseInsensitive-, filterWhereIdIn-, filterWhereNotIn-, filterWhereLike-, filterWhereILike-, filterWhereMatches-, filterWhereIMatches-, filterWhereJoinedTable-, filterWhereNotJoinedTable-, filterWhereInJoinedTable-, filterWhereNotInJoinedTable-, filterWhereLikeJoinedTable-, filterWhereILikeJoinedTable-, filterWhereMatchesJoinedTable-, filterWhereIMatchesJoinedTable-, filterWherePast-, filterWhereFuture-, labelResults-, EqOrIsOperator-, filterWhereSql-, FilterPrimaryKey (..)-, distinctOn-, distinct-, toSQL-, toSQL'-, buildQuery-, SQLQuery (..)-, OrderByClause (..)-, innerJoin-, innerJoinThirdTable-, HasQueryBuilder-, JoinQueryBuilderWrapper-, NoJoinQueryBuilderWrapper-, LabeledQueryBuilderWrapper-, getQueryBuilder-, NoJoins-, Condition (..)-, Join (..)-, OrderByDirection (..)-, injectQueryBuilder-, FilterOperator (..)-, toEqOrIsOperator-)-where-import IHP.Prelude-import Database.PostgreSQL.Simple.Types (In (In))-import Database.PostgreSQL.Simple.ToField-import IHP.ModelSupport-import qualified Data.ByteString.Builder as Builder-import IHP.HSX.ToHtml-import qualified Data.ByteString.Char8 as ByteString-import qualified Data.ByteString.Lazy as LByteString-import qualified Control.DeepSeq as DeepSeq-import qualified Data.Text.Encoding as Text-import Data.Text (toLower)-import qualified GHC.Generics--class DefaultScope table where- defaultScope :: QueryBuilder table -> QueryBuilder table--instance {-# OVERLAPPABLE #-} DefaultScope table where- {-# INLINE defaultScope #-}- defaultScope queryBuilder = queryBuilder--instance Table (GetModelByTableName table) => Default (QueryBuilder table) where- {-# INLINE def #-}- def = NewQueryBuilder { selectFrom = tableNameByteString @(GetModelByTableName table), columns = columnNames @(GetModelByTableName table) }--data MatchSensitivity = CaseSensitive | CaseInsensitive deriving (Show, Eq)--data FilterOperator- = EqOp -- ^ @col = val@- | NotEqOp -- ^ @col != val@- | InOp -- ^ @col IN (set)@- | NotInOp -- ^ @col NOT IN (set)@- | IsOp -- ^ @col IS val@- | IsNotOp -- ^ @col IS NOT val@- | LikeOp !MatchSensitivity -- ^ @col LIKE val@- | NotLikeOp !MatchSensitivity -- ^ @col NOT LIKE val@- | MatchesOp !MatchSensitivity -- ^ @col ~ pattern@- | SqlOp -- ^ Used by 'filterWhereSql'- deriving (Show, Eq)---compileOperator :: FilterOperator -> ByteString-compileOperator EqOp = "="-compileOperator NotEqOp = "!="-compileOperator InOp = "IN"-compileOperator NotInOp = "NOT IN"-compileOperator IsOp = "IS"-compileOperator IsNotOp = "IS NOT"-compileOperator (LikeOp CaseSensitive) = "LIKE"-compileOperator (LikeOp CaseInsensitive) = "ILIKE"-compileOperator (NotLikeOp CaseSensitive) = "NOT LIKE"-compileOperator (NotLikeOp CaseInsensitive) = "NOT ILIKE"-compileOperator (MatchesOp CaseSensitive) = " ~ "-compileOperator (MatchesOp CaseInsensitive) = " ~* "-compileOperator SqlOp = ""-{-# INLINE compileOperator #-}---- | Returns the "NOT" version of an operator------ >>> negateFilterOperator EqOp--- NotEqOp----negateFilterOperator :: FilterOperator -> FilterOperator-negateFilterOperator EqOp = NotEqOp-negateFilterOperator InOp = NotInOp-negateFilterOperator IsOp = IsNotOp-negateFilterOperator (LikeOp matchSensitivity) = (NotLikeOp matchSensitivity)-negateFilterOperator (MatchesOp matchSensitivity) = error "not supported"-negateFilterOperator SqlOp = SqlOp--data OrderByClause =- OrderByClause- { orderByColumn :: !ByteString- , orderByDirection :: !OrderByDirection }- deriving (Show, Eq, GHC.Generics.Generic, DeepSeq.NFData)---- Types implementing a type level list to record joined tables. EmptyModelList and ConsModelList correspond to the data constructors [] and :. NoJoins is like the empty List but cannot be extended.-data NoJoins-data EmptyModelList-data ConsModelList model models---- Type class to represent the true list type EmptyModelList ConsModelList.-class ModelList a--instance ModelList EmptyModelList-instance ModelList (ConsModelList model models)---- Typeclass to quer containment in the type-level list.-class IsJoined a b--instance (ModelList b) => IsJoined a (ConsModelList a b)-instance {-# OVERLAPPABLE #-} (ModelList b, IsJoined a b) => IsJoined a (ConsModelList c b)---- Class to generalise over different QueryBuilder-providing types. The actual query builder can be extracted with 'getQueryBuilder' and injected with 'injectQueryBuilder'. Also assigns a join reqister to a queryBilderProvider. -class HasQueryBuilder queryBuilderProvider joinRegister | queryBuilderProvider -> joinRegister where- getQueryBuilder :: queryBuilderProvider table -> QueryBuilder table- injectQueryBuilder :: QueryBuilder table -> queryBuilderProvider table- getQueryIndex :: queryBuilderProvider table -> Maybe ByteString- getQueryIndex _ = Nothing- {-# INLINABLE getQueryIndex #-}---- Wrapper for QueryBuilders resulting from joins. Associates a joinRegister type.-newtype JoinQueryBuilderWrapper joinRegister table = JoinQueryBuilderWrapper (QueryBuilder table)---- Wrapper for QueryBuilder that must not joins, e.g. queryUnion.-newtype NoJoinQueryBuilderWrapper table = NoJoinQueryBuilderWrapper (QueryBuilder table)---- Wrapper for QueryBuilders with indexed results.-newtype LabeledQueryBuilderWrapper foreignTable indexColumn indexValue table = LabeledQueryBuilderWrapper (QueryBuilder table)---- QueryBuilders have query builders and the join register is empty.-instance HasQueryBuilder QueryBuilder EmptyModelList where- getQueryBuilder = id- {-# INLINE getQueryBuilder #-}- injectQueryBuilder = id- {-# INLINE injectQueryBuilder #-}---- JoinQueryBuilderWrappers have query builders-instance HasQueryBuilder (JoinQueryBuilderWrapper joinRegister) joinRegister where- getQueryBuilder (JoinQueryBuilderWrapper queryBuilder) = queryBuilder- {-# INLINABLE getQueryBuilder #-}- injectQueryBuilder = JoinQueryBuilderWrapper - {-# INLINABLE injectQueryBuilder #-}---- NoJoinQueryBuilderWrapper have query builders and the join register does not allow any joins-instance HasQueryBuilder NoJoinQueryBuilderWrapper NoJoins where- getQueryBuilder (NoJoinQueryBuilderWrapper queryBuilder) = queryBuilder- {-# INLINABLE getQueryBuilder #-}- injectQueryBuilder = NoJoinQueryBuilderWrapper - {-# INLINABLE injectQueryBuilder #-}--instance (KnownSymbol foreignTable, foreignModel ~ GetModelByTableName foreignTable , KnownSymbol indexColumn, HasField indexColumn foreignModel indexValue) => HasQueryBuilder (LabeledQueryBuilderWrapper foreignTable indexColumn indexValue) NoJoins where- getQueryBuilder (LabeledQueryBuilderWrapper queryBuilder) = queryBuilder- {-# INLINABLE getQueryBuilder #-}- injectQueryBuilder = LabeledQueryBuilderWrapper- {-# INLINABLE injectQueryBuilder #-}- getQueryIndex _ = Just $ symbolToByteString @foreignTable <> "." <> (Text.encodeUtf8 . fieldNameToColumnName) (symbolToText @indexColumn)- {-# INLINABLE getQueryIndex #-}---data QueryBuilder (table :: Symbol) =- NewQueryBuilder { selectFrom :: !ByteString, columns :: ![ByteString] }- | DistinctQueryBuilder { queryBuilder :: !(QueryBuilder table) }- | DistinctOnQueryBuilder { queryBuilder :: !(QueryBuilder table), distinctOnColumn :: !ByteString }- | FilterByQueryBuilder { queryBuilder :: !(QueryBuilder table), queryFilter :: !(ByteString, FilterOperator, Action), applyLeft :: !(Maybe ByteString), applyRight :: !(Maybe ByteString) }- | OrderByQueryBuilder { queryBuilder :: !(QueryBuilder table), queryOrderByClause :: !OrderByClause }- | LimitQueryBuilder { queryBuilder :: !(QueryBuilder table), queryLimit :: !Int }- | OffsetQueryBuilder { queryBuilder :: !(QueryBuilder table), queryOffset :: !Int }- | UnionQueryBuilder { firstQueryBuilder :: !(QueryBuilder table), secondQueryBuilder :: !(QueryBuilder table) }- | JoinQueryBuilder { queryBuilder :: !(QueryBuilder table), joinData :: Join}- deriving (Show, Eq)--data Condition = VarCondition !ByteString !Action | OrCondition !Condition !Condition | AndCondition !Condition !Condition deriving (Show, Eq)---- | Display QueryBuilder's as their sql query inside HSX-instance KnownSymbol table => ToHtml (QueryBuilder table) where- toHtml queryBuilder = toHtml (toSQL queryBuilder)--data Join = Join { table :: ByteString, tableJoinColumn :: ByteString, otherJoinColumn :: ByteString }- deriving (Show, Eq)--data OrderByDirection = Asc | Desc deriving (Eq, Show, GHC.Generics.Generic, DeepSeq.NFData)-data SQLQuery = SQLQuery- { queryIndex :: !(Maybe ByteString)- , selectFrom :: !ByteString- , distinctClause :: !(Maybe ByteString)- , distinctOnClause :: !(Maybe ByteString)- , whereCondition :: !(Maybe Condition)- , joins :: ![Join]- , orderByClause :: ![OrderByClause]- , limitClause :: !(Maybe ByteString)- , offsetClause :: !(Maybe ByteString)- , columns :: ![ByteString]- } deriving (Show, Eq)---- | Needed for the 'Eq QueryBuilder' instance-deriving instance Eq Action---- | Need for the 'Eq QueryBuilder' instance------ You likely wonder: Why do we need the 'Eq SQLQuery' instance if this causes so much trouble?--- This has to do with how has-many and belongs-to relations are models by the SchemaCompiler------ E.g. given a table users and a table posts. Each Post belongs to a user. The schema compiler will--- add a field 'posts :: QueryBuilder "posts"' with the default value @query |> filterWhere (#userId, self.id)@ to all users by default.------ This is needed to support syntax like this:------ > fetch user.posts----instance Eq Builder.Builder where- a == b = (Builder.toLazyByteString a) == (Builder.toLazyByteString b)--instance SetField "queryIndex" SQLQuery (Maybe ByteString) where setField value sqlQuery = sqlQuery { queryIndex = value }-instance SetField "selectFrom" SQLQuery ByteString where setField value sqlQuery = sqlQuery { selectFrom = value }-instance SetField "distinctClause" SQLQuery (Maybe ByteString) where setField value sqlQuery = sqlQuery { distinctClause = value }-instance SetField "distinctOnClause" SQLQuery (Maybe ByteString) where setField value sqlQuery = sqlQuery { distinctOnClause = value }-instance SetField "whereCondition" SQLQuery (Maybe Condition) where setField value sqlQuery = sqlQuery { whereCondition = value }-instance SetField "orderByClause" SQLQuery [OrderByClause] where setField value sqlQuery = sqlQuery { orderByClause = value }-instance SetField "limitClause" SQLQuery (Maybe ByteString) where setField value sqlQuery = sqlQuery { limitClause = value }-instance SetField "offsetClause" SQLQuery (Maybe ByteString) where setField value sqlQuery = sqlQuery { offsetClause = value }------ | Represent's a @SELECT * FROM ..@ query. It's the starting point to build a query.--- Used together with the other functions to compose a sql query.------ Example:------ > toSQL (query @User)--- > -- Returns: ("SELECT id, firstname, lastname FROM users", [])------ Example: Fetching all users------ > allUsers <- query @User |> fetch--- > -- Runs a 'SELECT * FROM users' query------ You can use it together with 'filterWhere':------ > activeUsers :: [User] <---- > query @User--- > |> filterWhere (#active, True)--- > |> fetch-query :: forall model table. (table ~ GetTableName model, Table model) => DefaultScope table => QueryBuilder table-query = (defaultScope @table) NewQueryBuilder { selectFrom = tableNameByteString @model, columns = columnNames @model }-{-# INLINE query #-}--{-# INLINE buildQuery #-}-buildQuery :: forall table queryBuilderProvider joinRegister. (KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> SQLQuery-buildQuery queryBuilderProvider = buildQueryHelper $ getQueryBuilder queryBuilderProvider- where- buildQueryHelper NewQueryBuilder { selectFrom, columns } =- SQLQuery- { queryIndex = getQueryIndex queryBuilderProvider - , selectFrom = selectFrom- , distinctClause = Nothing- , distinctOnClause = Nothing- , whereCondition = Nothing- , joins = []- , orderByClause = []- , limitClause = Nothing- , offsetClause = Nothing- , columns- }- buildQueryHelper DistinctQueryBuilder { queryBuilder } = queryBuilder- |> buildQueryHelper- |> setJust #distinctClause "DISTINCT"- buildQueryHelper DistinctOnQueryBuilder { queryBuilder, distinctOnColumn } = queryBuilder- |> buildQueryHelper- |> setJust #distinctOnClause ("DISTINCT ON (" <> distinctOnColumn <> ")")- buildQueryHelper FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, operator, value), applyLeft, applyRight } =- let- applyFn fn value = case fn of- Just fn -> fn <> "(" <> value <> ")"- Nothing -> value-- condition = VarCondition (applyFn applyLeft columnName <> " " <> compileOperator operator <> " " <> applyFn applyRight "?") value- in- queryBuilder- |> buildQueryHelper- |> modify #whereCondition \case- Just c -> Just (AndCondition c condition)- Nothing -> Just condition- buildQueryHelper OrderByQueryBuilder { queryBuilder, queryOrderByClause } = queryBuilder- |> buildQueryHelper- |> modify #orderByClause (\value -> value <> [queryOrderByClause] ) -- although adding to the end of a list is bad form, these lists are very short- buildQueryHelper LimitQueryBuilder { queryBuilder, queryLimit } =- queryBuilder- |> buildQueryHelper- |> setJust #limitClause (- (Builder.byteString "LIMIT " <> Builder.intDec queryLimit)- |> Builder.toLazyByteString- |> LByteString.toStrict- )- buildQueryHelper OffsetQueryBuilder { queryBuilder, queryOffset } = queryBuilder- |> buildQueryHelper- |> setJust #offsetClause (- (Builder.byteString "OFFSET " <> Builder.intDec queryOffset)- |> Builder.toLazyByteString- |> LByteString.toStrict- )- buildQueryHelper UnionQueryBuilder { firstQueryBuilder, secondQueryBuilder } =- let- firstQuery = buildQueryHelper firstQueryBuilder- secondQuery = buildQueryHelper secondQueryBuilder- isSimpleQuery query = null (orderByClause query) && isNothing (limitClause query) && isNothing (offsetClause query) && null (joins query)- isSimpleUnion = isSimpleQuery firstQuery && isSimpleQuery secondQuery- unionWhere =- case (whereCondition firstQuery, whereCondition secondQuery) of- (Nothing, whereCondition) -> whereCondition- (whereCondition, Nothing) -> whereCondition- (Just firstWhere, Just secondWhere) -> Just $ OrCondition firstWhere secondWhere- in- if isSimpleUnion then- firstQuery { whereCondition = unionWhere }- else- error "buildQuery: Union of complex queries not supported yet"- - buildQueryHelper JoinQueryBuilder { queryBuilder, joinData } =- let - firstQuery = buildQueryHelper queryBuilder- in firstQuery { joins = joinData:joins firstQuery }- --- | Transforms a @query @@User |> ..@ expression into a SQL Query. Returns a tuple with the sql query template and it's placeholder values.------ __Example:__ Get the sql query that is represented by a QueryBuilder------ >>> let postsQuery = query @Post |> filterWhere (#public, True)--- >>> toSQL postsQuery--- ("SELECT posts.* FROM posts WHERE public = ?", [Plain "true"])-toSQL :: (KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> (ByteString, [Action])-toSQL queryBuilderProvider = toSQL' (buildQuery queryBuilderProvider)-{-# INLINE toSQL #-}--toSQL' :: SQLQuery -> (ByteString, [Action])-toSQL' sqlQuery@SQLQuery { queryIndex, selectFrom, distinctClause, distinctOnClause, orderByClause, limitClause, offsetClause, columns } =- (DeepSeq.force theQuery, theParams)- where- !theQuery =- ByteString.intercalate " " $- catMaybes- [ Just "SELECT"- , distinctClause- , distinctOnClause- , Just selectors- , Just "FROM"- , Just fromClause- , joinClause- , whereConditions'- , orderByClause'- , limitClause- , offsetClause- ]-- selectors :: ByteString- selectors = ByteString.intercalate ", " $ (catMaybes [queryIndex]) <> selectFromWithColumns- where- -- Generates a string like: `posts.id, posts.title, posts.body`- selectFromWithColumns :: [ByteString]- selectFromWithColumns = - columns- |> map (\column -> selectFrom <> "." <> column)- fromClause :: ByteString- fromClause = selectFrom-- !theParams =- case whereCondition sqlQuery of- Just condition -> compileConditionArgs condition- Nothing -> mempty-- toQualifiedName unqualifiedName = selectFrom <> "." <> unqualifiedName-- whereConditions' = case whereCondition sqlQuery of- Just condition -> Just $ "WHERE " <> compileConditionQuery condition- Nothing -> Nothing-- orderByClause' :: Maybe ByteString- orderByClause' = case orderByClause of- [] -> Nothing- xs -> Just ("ORDER BY " <> ByteString.intercalate "," ((map (\OrderByClause { orderByColumn, orderByDirection } -> orderByColumn <> (if orderByDirection == Desc then " DESC" else mempty)) xs)))- joinClause :: Maybe ByteString- joinClause = buildJoinClause $ reverse $ joins sqlQuery- buildJoinClause :: [Join] -> Maybe ByteString- buildJoinClause [] = Nothing- buildJoinClause (joinClause:joinClauses) = Just $ "INNER JOIN " <> table joinClause <> " ON " <> tableJoinColumn joinClause <> " = " <>table joinClause <> "." <> otherJoinColumn joinClause <> maybe "" (" " <>) (buildJoinClause joinClauses)---{-# INLINE toSQL' #-}--{-# INLINE compileConditionQuery #-}-compileConditionQuery :: Condition -> ByteString-compileConditionQuery (VarCondition var _) = var-compileConditionQuery (OrCondition a b) = "(" <> compileConditionQuery a <> ") OR (" <> compileConditionQuery b <> ")"-compileConditionQuery (AndCondition a b) = "(" <> compileConditionQuery a <> ") AND (" <> compileConditionQuery b <> ")"--{-# INLINE compileConditionArgs #-}-compileConditionArgs :: Condition -> [Action]-compileConditionArgs (VarCondition _ arg) = [arg]-compileConditionArgs (OrCondition a b) = compileConditionArgs a <> compileConditionArgs b-compileConditionArgs (AndCondition a b) = compileConditionArgs a <> compileConditionArgs b--class FilterPrimaryKey table where- filterWhereId :: Id' table -> QueryBuilder table -> QueryBuilder table---- | Adds a simple @WHERE x = y@ condition to the query.------ __Example:__ Only show projects where @active@ is @True@.------ > activeProjects <- query @Project--- > |> filterWhere (#active, True)--- > |> fetch--- > -- SELECT * FROM projects WHERE active = True------ __Example:__ Find book with title @Learn you a Haskell@.------ > book <- query @Book--- > |> filterWhere (#title, "Learn you a Haskell")--- > |> fetchOne--- > -- SELECT * FROM books WHERE name = 'Learn you a Haskell' LIMIT 1--------- __Example:__ Find active projects owned by the current user.------ > projects <- query @Project--- > |> filterWhere (#active, True)--- > |> filterWhere (#currentUserId, currentUserId)--- > |> fetch--- > -- SELECT * FROM projects WHERE active = true AND current_user_id = '..'--------- For dynamic conditions (e.g. involving @NOW()@), see 'filterWhereSql'.------ For @WHERE x IN (a, b, c)@ conditions, take a look at 'filterWhereIn' and 'filterWhereNotIn'.------ For @WHERE x LIKE a@ or @WHERE x ~ a@ conditions, see 'filterWhereLike' and 'filterWhereMatches' respectively.--- For case-insensitive versions of these operators, see 'filterWhereILike' and 'filterWhereIMatches'.------ When your condition is too complex, use a raw sql query with 'IHP.ModelSupport.sqlQuery'.-filterWhere :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, EqOrIsOperator value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhere (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, toEqOrIsOperator value, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhere #-}---- | Like 'filterWhere', but takes a type argument specifying the table which holds the column that is to be compared. The column must have been joined before using 'innerJoin' or 'innerJoinThirdTable'. Example: ------ __Example:__ get posts by user Tom.------ > tomPosts <- query @Post--- > |> innerJoin @User (#createdBy, #id)--- > |> filterWhereJoinedTable @User (#name, "Tom" :: Text)--- > |> fetch--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name = 'Tom'----filterWhereJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, EqOrIsOperator value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, toEqOrIsOperator value, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereJoinedTable #-}---- | Like 'filterWhere' but negates the condition.------ __Example:__ Only show projects created by other users.------ > activeProjects <- query @Project--- > |> filterWhereNot (#userId, currentUserId)--- > |> fetch--- > -- SELECT * FROM projects WHERE user_id != '23d5ea33-b28e-4f0a-99b3-77a3564a2546'----filterWhereNot :: forall name table model value. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, EqOrIsOperator value, model ~ GetModelByTableName table, Table model) => (Proxy name, value) -> QueryBuilder table -> QueryBuilder table-filterWhereNot (name, value) queryBuilder = FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, negateFilterOperator (toEqOrIsOperator value), toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))-{-# INLINE filterWhereNot #-}---- | Like 'filterWhereNotJoinedTable' but negates the condition.------ __Example:__ Only show projects not created by user Tom.------ > tomPosts <- query @Post--- > |> innerJoin @User (#createdBy, #id)--- > |> filterWhereNotJoinedTable @User (#name, "Tom" :: Text)--- > |> fetch--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name = 'Tom'----filterWhereNotJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, EqOrIsOperator value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereNotJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, negateFilterOperator (toEqOrIsOperator value), toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereNotJoinedTable #-}--- | Adds a @WHERE x IN (y)@ condition to the query.------ __Example:__ Only show projects where @status@ is @Draft@ or @Active@.------ > visibleProjects <- query @Project--- > |> filterWhereIn (#status, [Draft, Active])--- > |> fetch--- > -- SELECT * FROM projects WHERE status IN ('draft', 'active')------ For negation use 'filterWhereNotIn'----filterWhereIn :: forall name table model value queryBuilderProvider (joinRegister :: Type). (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, EqOrIsOperator value, Table model) => (Proxy name, [value]) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereIn (name, value) queryBuilderProvider =- case head nullValues of- Nothing -> injectQueryBuilder whereInQuery -- All values non null- Just nullValue ->- let- isNullValueExpr = FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, IsOp, toField nullValue), applyLeft = Nothing, applyRight = Nothing }- in- case head nonNullValues of- Just nonNullValue -> -- Some non null values, some null values- injectQueryBuilder $ UnionQueryBuilder- (injectQueryBuilder whereInQuery)- (injectQueryBuilder isNullValueExpr)- Nothing -> injectQueryBuilder isNullValueExpr -- All values null- where- -- Only NOT NULL values can be compares inside the IN expression, NULL values have to be compares using a manual appended IS expression- -- https://github.com/digitallyinduced/ihp/issues/906- --- (nonNullValues, nullValues) = value |> partition (\v -> toEqOrIsOperator v == EqOp)-- whereInQuery = FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, InOp, toField (In nonNullValues)), applyLeft = Nothing, applyRight = Nothing }-- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereIn #-}---- Like 'filterWhereIn', but case insensitive.------ __Example:__ Only show users where @email@ is @User1@example.com@ or @User2@example.com@.------ > users <- query @User--- > |> filterWhereInCaseInsensitive (#email, ['User1@example.com', 'User2@example.com'])--- > |> fetch--- > -- SELECT * FROM users WHERE LOWER(email) IN ('user1@example.com', 'user2@example.com')----filterWhereInCaseInsensitive :: forall name table model value queryBuilderProvider (joinRegister :: Type). ( KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, EqOrIsOperator value, Table model) => (Proxy name, [Text]) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereInCaseInsensitive (name, values) queryBuilderProvider =- case head nullValues of- Nothing -> injectQueryBuilder whereInQuery- Just nullValue ->- let- isNullValueExpr = FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, IsOp, toField nullValue), applyLeft = Nothing, applyRight = Nothing }- in- case head nonNullValues of- Just _ ->- injectQueryBuilder $ UnionQueryBuilder- (injectQueryBuilder whereInQuery)- (injectQueryBuilder isNullValueExpr)- Nothing -> injectQueryBuilder isNullValueExpr- where- (nonNullValues, nullValues) = values |> partition (\v -> toEqOrIsOperator v == EqOp)-- lowerValues = map toLower nonNullValues-- whereInQuery = FilterByQueryBuilder { queryBuilder, queryFilter = (lowerColumnName, InOp, toField (In lowerValues)), applyLeft = Nothing, applyRight = Nothing }-- lowerColumnName = "LOWER(" <> columnName <> ")"- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider--{-# INLINE filterWhereInCaseInsensitive #-}---- | Like 'filterWhereIn', but takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.--- --- __Example:__ get posts by Tom and Tim.------ > tomOrTimPosts <- query @Post --- > |> innerJoin @User (#createdBy, #id) --- > |> filterWhereInJoinedTable @User (#name, ["Tom","Tim"]) --- > |> fetch--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name IN ('Tom', 'Tim') ----filterWhereInJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, [value]) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereInJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, InOp, toField (In value)), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereInJoinedTable #-}----- | Adds a @WHERE x NOT IN (y)@ condition to the query.------ __Example:__ Only show projects where @status@ is not @Archived@------ > visibleProjects <- query @Project--- > |> filterWhereNotIn (#status, [Archived])--- > |> fetch--- > -- SELECT * FROM projects WHERE status NOT IN ('archived')------ The inclusive version of this function is called 'filterWhereIn'.----filterWhereNotIn :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, EqOrIsOperator value) => (Proxy name, [value]) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereNotIn (_, []) queryBuilder = queryBuilder -- Handle empty case by ignoring query part: `WHERE x NOT IN ()`-filterWhereNotIn (name, value) queryBuilderProvider =- case head nullValues of- Nothing -> injectQueryBuilder whereNotInQuery -- All values non null- Just nullValue ->- case head nonNullValues of- Just nonNullValue -> injectQueryBuilder FilterByQueryBuilder { queryBuilder = whereNotInQuery, queryFilter = (columnName, IsNotOp, toField nullValue), applyLeft = Nothing, applyRight = Nothing } -- Some non null values, some null values- Nothing -> injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, IsNotOp, toField nullValue), applyLeft = Nothing, applyRight = Nothing } -- All values null- where- -- Only NOT NULL values can be compares inside the IN expression, NULL values have to be compares using a manual appended IS expression- -- https://github.com/digitallyinduced/ihp/issues/906- --- (nonNullValues, nullValues) = value |> partition (\v -> toEqOrIsOperator v == EqOp)-- whereNotInQuery = FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, NotInOp, toField (In nonNullValues)), applyLeft = Nothing, applyRight = Nothing }-- columnName = Text.encodeUtf8 (symbolToText @table) <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereNotIn #-}---- | Like 'filterWhereNotIn', but takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.------ __Example:__ get posts by users not named Tom or Tim.------ > notTomOrTimPosts <- query @Post--- > |> innerJoin @User (#createdBy, #id)--- > |> filterWhereNotInJoinedTable @User (#name, ["Tom","Tim"])--- > |> fetch--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name NOT IN ('Tom', 'Tim')-filterWhereNotInJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, [value]) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereNotInJoinedTable (_, []) queryBuilderProvider = queryBuilderProvider -- Handle empty case by ignoring query part: `WHERE x NOT IN ()`-filterWhereNotInJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, NotInOp, toField (In value)), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereNotInJoinedTable #-}----- | Adds a @WHERE x LIKE y@ condition to the query.------ __Example:__ Find titles matching search term.------ > articles <- query @Article--- > |> filterWhereLike (#title, "%" <> searchTerm <> "%")--- > |> fetch--- > -- SELECT * FROM articles WHERE title LIKE '%..%'-filterWhereLike :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereLike (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, LikeOp CaseSensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereLike #-}---- | Like 'filterWhereLik'e, but takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.------ __Example:__ Serach for Posts by users whose name contains "olaf" (case insensitive)------ > olafPosts <- query @Post --- > |> innerJoin @User (#createdBy, #id) --- > |> filterWhereLikeJoinedTable @User (#name, "%Olaf%")--- > |> fetch--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name LIKE '%Olaf%'-filterWhereLikeJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol name, KnownSymbol table, table ~ GetTableName model, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereLikeJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, LikeOp CaseSensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereLikeJoinedTable #-}----- | Adds a @WHERE x ILIKE y@ condition to the query. Case-insensitive version of 'filterWhereLike'.------ __Example:__ Find titles matching search term.------ > articles <- query @Article--- > |> filterWhereILike (#title, "%" <> searchTerm <> "%")--- > |> fetch--- > -- SELECT * FROM articles WHERE title ILIKE '%..%'-filterWhereILike :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereILike (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, LikeOp CaseInsensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereILike #-}---- | Like 'filterWhereILike'; case-insensitive version of filterWhereLikeJoinedTable, takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.------ __Example:__ Serach for Posts by users whose name contains "olaf" (case insensitive)------ > olafPosts <- --- > query @Post --- |> innerJoin @User (#createdBy, #id) --- |> filterWhereILikeJoinedTable @User (#name, "%Olaf%")--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name ILIKE '%Olaf%' -filterWhereILikeJoinedTable :: forall model table name table' model' value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, table ~ GetTableName model, model' ~ GetModelByTableName table', HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereILikeJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, LikeOp CaseInsensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereILikeJoinedTable #-}----- | Adds a @WHERE x ~ y@ condition to the query.------ __Example:__ Find names with titles in front.------ > articles <- query @User--- > |> filterWhereMatches (#name, "^(M(rs|r|iss)|Dr|Sir). ")--- > |> fetch--- > -- SELECT * FROM articles WHERE title ~ '^(M(rs|r|iss)|Dr|Sir). '-filterWhereMatches :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereMatches (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, MatchesOp CaseSensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereMatches #-}---- | Adds a @WHERE x ~ y@ condition to the query, where the column x is held by a joined table.------ __Example:__ Find Posts by people with names with titles in front.------ > articles <- query @Post--- > |> innerJoin @User (#createdBy, #id)--- > |> filterWhereMatchesJoinedTable (#title, "^(M(rs|r|iss|s)|Dr|Sir). ")--- > |> fetch--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.title ~ '^(M(rs|r|iss|s)|Dr|Sir). '--filterWhereMatchesJoinedTable :: forall model table name value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereMatchesJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, MatchesOp CaseSensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereMatchesJoinedTable #-}----- | Adds a @WHERE x ~* y@ condition to the query. Case-insensitive version of 'filterWhereMatches'.-filterWhereIMatches :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereIMatches (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, MatchesOp CaseInsensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereIMatches #-}---- | Case-insensitive version of 'filterWhereMatchesJoinedTable'-filterWhereIMatchesJoinedTable :: forall model table name value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'-filterWhereIMatchesJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, MatchesOp CaseInsensitive, toField value), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereIMatchesJoinedTable #-}----- | Filter all rows by whether a field is in the past, determined by comparing 'NOW()' to the field's value.------ Opposite of 'filterWhereFuture'------ __Example:__ Fetch all posts scheduled for the past.------ > publicPosts <- query @Post--- > |> filterWherePast #scheduledAt--- > |> fetch--- > -- SELECT * FROM posts WHERE scheduled_at <= NOW()-filterWherePast- :: ( KnownSymbol table- , KnownSymbol name- , ToField value- , HasField name (GetModelByTableName table) value- , HasQueryBuilder queryBuilderProvider joinRegister- , Table (GetModelByTableName table)- )- => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table-filterWherePast name = filterWhereSql (name, "<= NOW()")-{-# INLINE filterWherePast #-}---- | Filter all rows by whether a field is in the future, determined by comparing 'NOW()' to the field's value.------ Opposite of 'filterWherePast'------ __Example:__ Fetch all posts scheduled for the future.------ > hiddenPosts <- query @Post--- > |> filterWhereFuture #scheduledAt--- > |> fetch--- > -- SELECT * FROM posts WHERE scheduled_at > NOW()-filterWhereFuture- :: ( KnownSymbol table- , KnownSymbol name- , ToField value- , HasField name (GetModelByTableName table) value- , HasQueryBuilder queryBuilderProvider joinRegister- , Table (GetModelByTableName table)- )- => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereFuture name = filterWhereSql (name, "> NOW()")-{-# INLINE filterWhereFuture #-}----- | Allows to add a custom raw sql where condition------ If your query cannot be represented with 'filterWhereSql', take a look at 'IHP.ModelSupport.sqlQuery'.------ __Example:__ Fetching all projects created in the last 24 hours.------ > latestProjects <- query @Project--- > |> filterWhereSql (#startedAt, "< current_timestamp - interval '1 day'")--- > |> fetch--- > -- SELECT * FROM projects WHERE started_at < current_timestamp - interval '1 day'----filterWhereSql :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, ByteString) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereSql (name, sqlCondition) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, SqlOp, Plain (Builder.byteString sqlCondition)), applyLeft = Nothing, applyRight = Nothing }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereSql #-}---- | Adds a @WHERE LOWER(x) = LOWER(y)@ condition to the query.------ __Example:__ Get a user by an email address, ignoring case------ > user <- query @User--- > |> filterWhereCaseInsensitive (#email, "marc@digitallyinduced.com")--- > |> fetchOne--- > -- SELECT * FROM users WHERE LOWER(email) = 'marc@digitallyinduced.com'------ For high performance it's best to have an index for @LOWER(field)@ in your Schema.sql------ >>> CREATE UNIQUE INDEX users_email_index ON users ((LOWER(email)));----filterWhereCaseInsensitive :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, ToField value, HasField name model value, EqOrIsOperator value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereCaseInsensitive (name, value) queryBuilderProvider = injectQueryBuilder FilterByQueryBuilder { queryBuilder, queryFilter = (columnName, toEqOrIsOperator value, toField value), applyLeft = Just "LOWER", applyRight = Just "LOWER" }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE filterWhereCaseInsensitive #-}---filterWhereIdIn :: forall table model queryBuilderProvider (joinRegister :: Type). (KnownSymbol table, Table model, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister) => [Id model] -> queryBuilderProvider table -> queryBuilderProvider table-filterWhereIdIn values queryBuilderProvider =- -- We don't need to treat null values differently here, because primary keys imply not-null- let- pkConditions = map (primaryKeyConditionForId @model) values-- queryBuilder = getQueryBuilder queryBuilderProvider-- whereInQuery = FilterByQueryBuilder {queryBuilder, queryFilter = (primaryKeyConditionColumnSelector @model, InOp, toField (In pkConditions)), applyLeft = Nothing, applyRight = Nothing}- in- injectQueryBuilder whereInQuery-{-# INLINE filterWhereIdIn #-}----- | Joins a table to an existing QueryBuilder (or something holding a QueryBuilder) on the specified columns. Example:--- > query @Posts --- > |> innerJoin @Users (#author, #id)--- > -- SELECT users.* FROM users INNER JOIN posts ON users.id = posts.author ...-innerJoin :: forall model' table' name' value' model table name value queryBuilderProvider joinRegister.- (- KnownSymbol name, - KnownSymbol table,- HasField name model value,- KnownSymbol name', - KnownSymbol table',- HasQueryBuilder queryBuilderProvider joinRegister,- ModelList joinRegister,- HasField name' model' value', - value ~ value',- model ~ GetModelByTableName table,- table' ~ GetTableName model'- ) => (Proxy name, Proxy name') -> queryBuilderProvider table -> JoinQueryBuilderWrapper (ConsModelList model' joinRegister) table -innerJoin (name, name') queryBuilderProvider = injectQueryBuilder $ JoinQueryBuilder (getQueryBuilder queryBuilderProvider) $ Join joinTableName leftJoinColumn rightJoinColumn - where - baseTableName = symbolToByteString @table- joinTableName = symbolToByteString @table'- leftJoinColumn = baseTableName <> "." <> (Text.encodeUtf8 . fieldNameToColumnName) (symbolToText @name)- rightJoinColumn = (Text.encodeUtf8 . fieldNameToColumnName) (symbolToText @name')-{-# INLINE innerJoin #-}---- | Index the values from a table with values of a field from a table joined by 'innerJoin' or 'innerJoinThirdTable'. Useful to get, e.g., the tags to a set of posts in such a way that the assignment of tags to posts is preserved.--------- __Example:__ Fetch a list of all comments, each paired with the id of the post it belongs to.------ > labeledTags <---- > query @Tag--- > |> innerJoin @Tagging (#id, #tagId)--- > |> innerJoinThirdTable @Post @Tagging (#id, #postId)--- > |> labelResults @Post #id--- > |> fetch--- > -- SELECT posts.id, tags.* FROM comments INNER JOIN taggings ON tags.id = taggings.tagId INNER JOIN posts ON posts.id = taggings.postId --- --- labeledTags is then a list of type ['LabeledData' (Id' "posts") Tag] such that "LabeledData postId tag" is contained in that list if "tag" is a tag of the post with id postId.----labelResults :: forall foreignModel baseModel foreignTable baseTable name value queryBuilderProvider joinRegister.- (- KnownSymbol foreignTable,- KnownSymbol baseTable,- foreignTable ~ GetTableName foreignModel, - baseModel ~ GetModelByTableName baseTable,- HasField name foreignModel value,- HasQueryBuilder queryBuilderProvider joinRegister,- KnownSymbol name,- IsJoined foreignModel joinRegister- ) => Proxy name -> queryBuilderProvider baseTable -> LabeledQueryBuilderWrapper foreignTable name value baseTable-labelResults name queryBuilderProvider = LabeledQueryBuilderWrapper $ getQueryBuilder queryBuilderProvider- --- | Joins a table on a column held by a previously joined table. Example:--- > query @Posts --- > |> innerJoin @Users (#author, #id)--- > |> innerJoinThirdTable @City @Users (#id, #homeTown)--- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.author = users.id INNER JOIN cities ON user.home_town = cities.id----innerJoinThirdTable :: forall model model' name name' value value' table table' baseTable baseModel queryBuilderProvider joinRegister.- ( - KnownSymbol name,- KnownSymbol table,- HasField name model value,- KnownSymbol name',- KnownSymbol table',- HasQueryBuilder queryBuilderProvider joinRegister,- ModelList joinRegister,- HasField name' model' value',- value ~ value',- table ~ GetTableName model,- table' ~ GetTableName model',- baseModel ~ GetModelByTableName baseTable - ) => (Proxy name, Proxy name') -> queryBuilderProvider baseTable -> JoinQueryBuilderWrapper (ConsModelList model joinRegister) baseTable-innerJoinThirdTable (name, name') queryBuilderProvider = injectQueryBuilder $ JoinQueryBuilder (getQueryBuilder queryBuilderProvider) $ Join joinTableName leftJoinColumn rightJoinColumn- where - baseTableName = symbolToByteString @table'- joinTableName = symbolToByteString @table- leftJoinColumn = baseTableName <> "." <> (Text.encodeUtf8 . fieldNameToColumnName) (symbolToText @name')- rightJoinColumn = (Text.encodeUtf8 . fieldNameToColumnName) (symbolToText @name)-{-# INLINE innerJoinThirdTable #-}---- | Adds an @ORDER BY .. ASC@ to your query.------ Use 'orderByDesc' for descending order.------ __Example:__ Fetch the 10 oldest books.------ > query @Book--- > |> orderBy #createdAt -- > |> limit 10--- > |> fetch--- > -- SELECT * FROM books LIMIT 10 ORDER BY created_at ASC-orderByAsc :: forall name model table value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table-orderByAsc !name queryBuilderProvider = injectQueryBuilder OrderByQueryBuilder { queryBuilder, queryOrderByClause = OrderByClause { orderByColumn = columnName, orderByDirection = Asc } }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE orderByAsc #-}---- | Adds an @ORDER BY .. ASC@ on a joined table column to your query.------ Use 'orderByDescJoinedTable' for descending order.------ __Example:__ Order joined `User` records by `username` ascending.------ > query @Project--- > |> innerJoin @User (#id, #projectId)--- > |> orderByAscJoinedTable #username--- > |> fetch--- > -- SELECT ... FROM projects--- > -- INNER JOIN users ON projects.id = users.project_id--- > -- ORDER BY users.username ASC-orderByAscJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. ( KnownSymbol table, KnownSymbol name, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => Proxy name -> queryBuilderProvider table' -> queryBuilderProvider table'-orderByAscJoinedTable !name queryBuilderProvider = injectQueryBuilder OrderByQueryBuilder { queryBuilder = queryBuilder, queryOrderByClause = OrderByClause { orderByColumn = columnName, orderByDirection = Asc } }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE orderByAscJoinedTable #-}---- | Adds an @ORDER BY .. DESC@ to your query.------ Use 'orderBy' for ascending order.------ __Example:__ Fetch the 10 newest projects (ordered by creation time).------ > query @Project--- > |> orderByDesc #createdAt--- > |> limit 10--- > |> fetch--- > -- SELECT * FROM projects LIMIT 10 ORDER BY created_at DESC-orderByDesc :: forall name model table value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table-orderByDesc !name queryBuilderProvider = injectQueryBuilder OrderByQueryBuilder { queryBuilder, queryOrderByClause = OrderByClause { orderByColumn = columnName, orderByDirection = Desc } }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE orderByDesc #-}---- | Adds an @ORDER BY .. DESC@ on a joined table column to your query.------ Use 'orderByAscJoinedTable' for ascending order.------ __Example:__ Order joined `User` records by `username` descending.------ > query @Project--- > |> innerJoin @User (#id, #projectId)--- > |> orderByDescJoinedTable #username--- > |> fetch--- > -- SELECT ... FROM projects--- > -- INNER JOIN users ON projects.id = users.project_id--- > -- ORDER BY users.username DESC-orderByDescJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. ( KnownSymbol table, KnownSymbol name, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => Proxy name -> queryBuilderProvider table' -> queryBuilderProvider table'-orderByDescJoinedTable !name queryBuilderProvider = injectQueryBuilder OrderByQueryBuilder { queryBuilder = queryBuilder, queryOrderByClause = OrderByClause { orderByColumn = columnName, orderByDirection = Desc } }- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE orderByDescJoinedTable #-}---- | Alias for 'orderByAsc'-orderBy :: (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table-orderBy !name = orderByAsc name-{-# INLINE orderBy #-}---- | Alias for 'orderByAscJoinedTable'-orderByJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => Proxy name -> queryBuilderProvider table' -> queryBuilderProvider table'-orderByJoinedTable !name = orderByAscJoinedTable @model @name @table @value @queryBuilderProvider @joinRegister @table' name-{-# INLINE orderByJoinedTable #-}---- | Adds an @LIMIT ..@ to your query.--------- __Example:__ Fetch 10 posts------ > query @Post--- > |> limit 10--- > |> fetch--- > -- SELECT * FROM posts LIMIT 10-limit :: (HasQueryBuilder queryBuilderProvider joinRegister) => Int -> queryBuilderProvider model -> queryBuilderProvider model-limit !queryLimit queryBuilderProvider = injectQueryBuilder LimitQueryBuilder { queryBuilder, queryLimit }- where- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE limit #-}---- | Adds an @OFFSET ..@ to your query. Most often used together with @LIMIT...@--------- __Example:__ Fetch posts 10-20------ > query @Post--- > |> limit 10--- > |> offset 10--- > |> fetch--- > -- SELECT * FROM posts LIMIT 10 OFFSET 10-offset :: (HasQueryBuilder queryBuilderProvider joinRegister) => Int -> queryBuilderProvider model -> queryBuilderProvider model-offset !queryOffset queryBuilderProvider = injectQueryBuilder OffsetQueryBuilder { queryBuilder, queryOffset }- where- queryBuilder = getQueryBuilder queryBuilderProvider-{-# INLINE offset #-}---- | Merges the results of two query builders.------ Take a look at ‘queryOr' as well, as this might be a bit shorter.------ __Example:__ Return all pages owned by the user or owned by the users team.------ > let userPages = query @Page |> filterWhere (#ownerId, currentUserId)--- > let teamPages = query @Page |> filterWhere (#teamId, currentTeamId)--- > pages <- queryUnion userPages teamPages |> fetch--- > -- (SELECT * FROM pages WHERE owner_id = '..') UNION (SELECT * FROM pages WHERE team_id = '..')-queryUnion :: (HasQueryBuilder queryBuilderProvider joinRegister, HasQueryBuilder r joinRegister') => queryBuilderProvider model -> r model -> NoJoinQueryBuilderWrapper model-queryUnion firstQueryBuilderProvider secondQueryBuilderProvider = NoJoinQueryBuilderWrapper (UnionQueryBuilder { firstQueryBuilder, secondQueryBuilder })- where- firstQueryBuilder = getQueryBuilder firstQueryBuilderProvider- secondQueryBuilder = getQueryBuilder secondQueryBuilderProvider-- -{-# INLINE queryUnion #-}---- | Like 'queryUnion', but applied on all the elements on the list------ > action ProjectsAction = do--- > let values :: [(ProjectType, Int)] = [(ProjectTypeOngoing, 3), (ProjectTypeNotStarted, 2)]--- >--- > valuePairToCondition :: (ProjectType, Int) -> QueryBuilder "projects"--- > valuePairToCondition (projectType, participants) =--- > query @Project--- > |> filterWhere (#projectType, projectType)--- > |> filterWhere (#participants, participants)--- >--- > theQuery = queryUnionList (map valuePairToCondition values)--- >--- > projects <- fetch theQuery--- > render IndexView { .. }-queryUnionList :: forall table. (Table (GetModelByTableName table), KnownSymbol table, GetTableName (GetModelByTableName table) ~ table) => [QueryBuilder table] -> QueryBuilder table-queryUnionList [] = FilterByQueryBuilder { queryBuilder = query @(GetModelByTableName table) @table, queryFilter = ("id", NotEqOp, Plain "id"), applyLeft = Nothing, applyRight = Nothing }-queryUnionList (firstQueryBuilder:secondQueryBuilder:[]) = UnionQueryBuilder { firstQueryBuilder, secondQueryBuilder }-queryUnionList (firstQueryBuilder:rest) = UnionQueryBuilder { firstQueryBuilder, secondQueryBuilder = queryUnionList @table rest }----- | Adds an @a OR b@ condition------ __Example:__ Return all pages owned by the user or public.------ > query @Page--- > |> queryOr--- > (filterWhere (#createdBy, currentUserId))--- > (filterWhere (#public, True))--- > |> fetch--- > -- SELECT * FROM pages WHERE created_by = '..' OR public = True-queryOr :: (HasQueryBuilder queryBuilderProvider joinRegister, HasQueryBuilder queryBuilderProvider'' joinRegister'', HasQueryBuilder queryBuilderProvider''' joinRegister''') => (queryBuilderProvider model -> queryBuilderProvider''' model) -> (queryBuilderProvider model -> queryBuilderProvider'' model) -> queryBuilderProvider model -> queryBuilderProvider model-queryOr firstQuery secondQuery queryBuilder = injectQueryBuilder- (UnionQueryBuilder { - firstQueryBuilder = getQueryBuilder $ firstQuery queryBuilder, - secondQueryBuilder = getQueryBuilder $ secondQuery queryBuilder}- )-{-# INLINE queryOr #-}---- | Adds a @DISTINCT@ to your query.------ Use 'distinct' to remove all duplicate rows from the result------ __Example:__ Fetch distinct books------ > query @Book--- > |> distinct--- > |> fetch--- > -- SELECT DISTINCT * FROM books-distinct :: (HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> queryBuilderProvider table-distinct = injectQueryBuilder . DistinctQueryBuilder . getQueryBuilder-{-# INLINE distinct #-}---- | Adds an @DISTINCT ON .. to your query.------ Use 'distinctOn' to return a single row for each distinct value provided.------ __Example:__ Fetch one book for each categoryId field------ > query @Book--- > |> distinctOn #categoryId--- > |> fetch--- > -- SELECT DISTINCT ON (category_id) * FROM books-distinctOn :: forall name model value table queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table-distinctOn !name queryBuilderProvider = injectQueryBuilder DistinctOnQueryBuilder { distinctOnColumn = columnName, queryBuilder = getQueryBuilder queryBuilderProvider}- where- columnName = tableNameByteString @model <> "." <> Text.encodeUtf8 (fieldNameToColumnName (symbolToText @name))-{-# INLINE distinctOn #-}------ | Helper to deal with @some_field IS NULL@ and @some_field = 'some value'@-class EqOrIsOperator value where toEqOrIsOperator :: value -> FilterOperator-instance {-# OVERLAPS #-} EqOrIsOperator (Maybe something) where toEqOrIsOperator Nothing = IsOp; toEqOrIsOperator (Just _) = EqOp-instance {-# OVERLAPPABLE #-} EqOrIsOperator otherwise where toEqOrIsOperator _ = EqOp+{-|+Module: IHP.QueryBuilder+Description: Tool to build simple sql queries+Copyright: (c) digitally induced GmbH, 2020++QueryBuilder is mainly used for doing simple @SELECT@ sql queries. It allows dynamic+creation of sql queries in a type safe way.++For more complex sql queries, use 'IHP.ModelSupport.sqlQuery'.++This module re-exports all QueryBuilder submodules for backward compatibility.+-}+module IHP.QueryBuilder+( -- * Starting a Query+ query+ -- * Core Types+, QueryBuilder (..)+, Condition (..)+, ConditionValue (..)+, Join (..)+, OrderByClause (..)+, OrderByDirection (..)+, FilterOperator (..)+, MatchSensitivity (..)+ -- * Type Classes+, DefaultScope (..)+, HasQueryBuilder+, EqOrIsOperator+, FilterPrimaryKey (..)+ -- * QueryBuilder Wrappers+, JoinQueryBuilderWrapper+, NoJoinQueryBuilderWrapper+, LabeledQueryBuilderWrapper+, getQueryBuilder+, injectQueryBuilder+ -- * Type-level Join Tracking+, NoJoins+ -- * SQL Compilation+, buildQuery+ -- * Hasql Compilation+, toSQL+ -- * Filtering+, filterWhere+, filterWhereCaseInsensitive+, filterWhereNot+, filterWhereIn+, filterWhereInCaseInsensitive+, filterWhereIdIn+, filterWhereNotIn+, filterWhereLike+, filterWhereILike+, filterWhereMatches+, filterWhereIMatches+, filterWherePast+, filterWhereFuture+, filterWhereGreaterThan+, filterWhereLarger+, filterWhereGreaterThanOrEqualTo+, filterWhereAtLeast+, filterWhereLessThan+, filterWhereSmaller+, filterWhereLessThanOrEqualTo+, filterWhereAtMost+, filterWhereSql+ -- * Filtering on Joined Tables+, filterWhereJoinedTable+, filterWhereCaseInsensitiveJoinedTable+, filterWhereNotJoinedTable+, filterWhereInJoinedTable+, filterWhereNotInJoinedTable+, filterWhereLikeJoinedTable+, filterWhereILikeJoinedTable+, filterWhereMatchesJoinedTable+, filterWhereIMatchesJoinedTable+ -- * Joins+, innerJoin+, innerJoinThirdTable+, labelResults+ -- * Ordering+, orderBy+, orderByAsc+, orderByDesc+, orderByJoinedTable+, orderByAscJoinedTable+, orderByDescJoinedTable+ -- * Pagination+, limit+, offset+ -- * Deduplication+, distinct+, distinctOn+ -- * Unions+, queryUnion+, queryUnionList+, queryOr+ -- * Operators+, toEqOrIsOperator+, compileOperator+, negateFilterOperator+) where++import IHP.QueryBuilder.Types+import IHP.QueryBuilder.Compiler+import IHP.QueryBuilder.HasqlCompiler (toSQL, compileOperator)+import IHP.QueryBuilder.Filter+import IHP.QueryBuilder.Join+import IHP.QueryBuilder.Order+import IHP.QueryBuilder.Union
@@ -0,0 +1,89 @@+{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}+{-|+Module: IHP.QueryBuilder.Compiler+Description: SQL compilation for QueryBuilder+Copyright: (c) digitally induced GmbH, 2020++This module provides functions to compile a QueryBuilder into SQL.+-}+module IHP.QueryBuilder.Compiler+( query+, buildQuery+, negateFilterOperator+, qualifiedColumnName+) where++import IHP.Prelude+import IHP.ModelSupport+import IHP.QueryBuilder.Types+++-- | Returns the "NOT" version of an operator+--+-- >>> negateFilterOperator EqOp+-- NotEqOp+--+negateFilterOperator :: FilterOperator -> FilterOperator+negateFilterOperator EqOp = NotEqOp+negateFilterOperator NotEqOp = EqOp+negateFilterOperator InOp = NotInOp+negateFilterOperator NotInOp = InOp+negateFilterOperator IsOp = IsNotOp+negateFilterOperator IsNotOp = IsOp+negateFilterOperator (LikeOp matchSensitivity) = (NotLikeOp matchSensitivity)+negateFilterOperator (NotLikeOp matchSensitivity) = (LikeOp matchSensitivity)+negateFilterOperator (MatchesOp matchSensitivity) = error "not supported"+negateFilterOperator GreaterThanOp = LessThanOrEqualToOp+negateFilterOperator GreaterThanOrEqualToOp = LessThanOp+negateFilterOperator LessThanOp = GreaterThanOrEqualToOp+negateFilterOperator LessThanOrEqualToOp = GreaterThanOp+negateFilterOperator SqlOp = SqlOp++-- | Represent's a @SELECT * FROM ..@ query. It's the starting point to build a query.+-- Used together with the other functions to compose a sql query.+--+-- Example: Fetching all users+--+-- > allUsers <- query @User |> fetch+-- > -- Runs a 'SELECT * FROM users' query+--+-- You can use it together with 'filterWhere':+--+-- > activeUsers :: [User] <-+-- > query @User+-- > |> filterWhere (#active, True)+-- > |> fetch+query :: forall model table. (table ~ GetTableName model, Table model) => DefaultScope table => QueryBuilder table+query = let tn = tableName @model+ cols = columnNames @model+ in (defaultScope @table) QueryBuilder { unQueryBuilder = SQLQuery+ { queryIndex = Nothing+ , selectFrom = tn+ , columns = cols+ , columnsSql = qualifyAndJoinColumns tn cols+ , distinctClause = False+ , distinctOnClause = Nothing+ , whereCondition = Nothing+ , joins = []+ , orderByClause = []+ , limitClause = Nothing+ , offsetClause = Nothing+ } }+{-# INLINE query #-}++-- | Extract the SQLQuery from a QueryBuilder provider, setting the queryIndex.+{-# INLINE buildQuery #-}+buildQuery :: forall table queryBuilderProvider joinRegister. (KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> SQLQuery+buildQuery queryBuilderProvider = (unQueryBuilder (getQueryBuilder queryBuilderProvider)) { queryIndex = getQueryIndex queryBuilderProvider }++-- | Build a qualified column name like @tablename.column_name@ from a table name+-- and a camelCase field name. The field name is converted to snake_case via+-- 'fieldNameToColumnName'.+--+-- This is intentionally NOINLINE: the call sites in filterWhere, orderBy, etc.+-- are always lifted to CAFs (evaluated once at program start), so inlining+-- only duplicates the Text.Inflections parsing logic and Text concatenation+-- without any runtime benefit.+qualifiedColumnName :: Text -> Text -> Text+qualifiedColumnName tableName fieldName = tableName <> "." <> fieldNameToColumnName fieldName+{-# NOINLINE qualifiedColumnName #-}
@@ -0,0 +1,633 @@+{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}+{-|+Module: IHP.QueryBuilder.Filter+Description: WHERE clause filtering functions for QueryBuilder+Copyright: (c) digitally induced GmbH, 2020++This module provides all the filterWhere* functions for building WHERE clauses.+-}+module IHP.QueryBuilder.Filter+( filterWhere+, filterWhereCaseInsensitive+, filterWhereNot+, filterWhereIn+, filterWhereInCaseInsensitive+, filterWhereIdIn+, filterWhereNotIn+, filterWhereLike+, filterWhereILike+, filterWhereMatches+, filterWhereIMatches+, filterWherePast+, filterWhereFuture+, filterWhereGreaterThan+, filterWhereLarger+, filterWhereGreaterThanOrEqualTo+, filterWhereAtLeast+, filterWhereLessThan+, filterWhereSmaller+, filterWhereLessThanOrEqualTo+, filterWhereAtMost+, filterWhereSql+ -- * Joined table variants+, filterWhereJoinedTable+, filterWhereCaseInsensitiveJoinedTable+, filterWhereNotJoinedTable+, filterWhereInJoinedTable+, filterWhereNotInJoinedTable+, filterWhereLikeJoinedTable+, filterWhereILikeJoinedTable+, filterWhereMatchesJoinedTable+, filterWhereIMatchesJoinedTable+) where++import IHP.Prelude+import IHP.ModelSupport+import IHP.QueryBuilder.Types+import IHP.QueryBuilder.Compiler (negateFilterOperator)+import qualified Hasql.Encoders as Encoders+import Hasql.Implicits.Encoders (DefaultParamEncoder(..))+import IHP.Hasql.Encoders () -- Import for DefaultParamEncoder instances+import IHP.QueryBuilder.Compiler (qualifiedColumnName)+import Data.Functor.Contravariant (contramap)++-- | Build a 'ConditionValue' from any 'DefaultParamEncoder' value.+paramValue :: DefaultParamEncoder a => a -> ConditionValue+paramValue value = Param (contramap (const value) (Encoders.param defaultParam))+{-# INLINE paramValue #-}++-- | Build the appropriate 'ConditionValue' based on the operator.+-- For 'IsOp'/'IsNotOp', returns @Literal "NULL"@ (no parameter consumed).+-- For all other operators, returns @Param encoder@.+condValueForOp :: DefaultParamEncoder a => FilterOperator -> a -> ConditionValue+condValueForOp IsOp _ = Literal "NULL"+condValueForOp IsNotOp _ = Literal "NULL"+condValueForOp _ value = paramValue value+{-# INLINE condValueForOp #-}++-- | Adds a simple @WHERE x = y@ condition to the query.+--+-- __Example:__ Only show projects where @active@ is @True@.+--+-- > activeProjects <- query @Project+-- > |> filterWhere (#active, True)+-- > |> fetch+-- > -- SELECT * FROM projects WHERE active = True+--+-- __Example:__ Find book with title @Learn you a Haskell@.+--+-- > book <- query @Book+-- > |> filterWhere (#title, "Learn you a Haskell")+-- > |> fetchOne+-- > -- SELECT * FROM books WHERE name = 'Learn you a Haskell' LIMIT 1+--+--+-- __Example:__ Find active projects owned by the current user.+--+-- > projects <- query @Project+-- > |> filterWhere (#active, True)+-- > |> filterWhere (#currentUserId, currentUserId)+-- > |> fetch+-- > -- SELECT * FROM projects WHERE active = true AND current_user_id = '..'+--+--+-- For dynamic conditions (e.g. involving @NOW()@), see 'filterWhereSql'.+--+-- For @WHERE x IN (a, b, c)@ conditions, take a look at 'filterWhereIn' and 'filterWhereNotIn'.+--+-- For @WHERE x LIKE a@ or @WHERE x ~ a@ conditions, see 'filterWhereLike' and 'filterWhereMatches' respectively.+-- For case-insensitive versions of these operators, see 'filterWhereILike' and 'filterWhereIMatches'.+--+-- When your condition is too complex, use a raw sql query with 'IHP.ModelSupport.sqlQuery'.+filterWhere :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, EqOrIsOperator value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhere (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = let op = toEqOrIsOperator value in ColumnCondition columnName op (condValueForOp op value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhere #-}++-- | Like 'filterWhere', but takes a type argument specifying the table which holds the column that is to be compared. The column must have been joined before using 'innerJoin' or 'innerJoinThirdTable'. Example:+--+-- __Example:__ get posts by user Tom.+--+-- > tomPosts <- query @Post+-- > |> innerJoin @User (#createdBy, #id)+-- > |> filterWhereJoinedTable @User (#name, "Tom" :: Text)+-- > |> fetch+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name = 'Tom'+--+filterWhereJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, EqOrIsOperator value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = let op = toEqOrIsOperator value in ColumnCondition columnName op (condValueForOp op value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereJoinedTable #-}++-- | Like 'filterWhereJoinedTable', but adds a @WHERE LOWER(x) = LOWER(y)@ condition to the query. Example:+--+-- __Example:__ get posts by user Tom, ignoring case.+--+-- > tomPosts <- query @Post+-- > |> innerJoin @User (#createdBy, #id)+-- > |> filterWhereCaseInsensitiveJoinedTable @User (#name, "Tom" :: Text)+-- > |> fetch+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE LOWER(users.name) = LOWER('Tom')+--+filterWhereCaseInsensitiveJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, EqOrIsOperator value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereCaseInsensitiveJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = let op = toEqOrIsOperator value in ColumnCondition columnName op (condValueForOp op value) (Just "LOWER") (Just "LOWER")+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereCaseInsensitiveJoinedTable #-}++-- | Like 'filterWhere' but negates the condition.+--+-- __Example:__ Only show projects created by other users.+--+-- > activeProjects <- query @Project+-- > |> filterWhereNot (#userId, currentUserId)+-- > |> fetch+-- > -- SELECT * FROM projects WHERE user_id != '23d5ea33-b28e-4f0a-99b3-77a3564a2546'+--+filterWhereNot :: forall name table model value. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, EqOrIsOperator value, model ~ GetModelByTableName table, Table model) => (Proxy name, value) -> QueryBuilder table -> QueryBuilder table+filterWhereNot (name, value) queryBuilder = addCondition condition queryBuilder+ where+ condition = let op = negateFilterOperator (toEqOrIsOperator value) in ColumnCondition columnName op (condValueForOp op value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereNot #-}++-- | Like 'filterWhereNotJoinedTable' but negates the condition.+--+-- __Example:__ Only show projects not created by user Tom.+--+-- > tomPosts <- query @Post+-- > |> innerJoin @User (#createdBy, #id)+-- > |> filterWhereNotJoinedTable @User (#name, "Tom" :: Text)+-- > |> fetch+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name = 'Tom'+--+filterWhereNotJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, EqOrIsOperator value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereNotJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = let op = negateFilterOperator (toEqOrIsOperator value) in ColumnCondition columnName op (condValueForOp op value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereNotJoinedTable #-}+-- | Adds a @WHERE x IN (y)@ condition to the query.+--+-- __Example:__ Only show projects where @status@ is @Draft@ or @Active@.+--+-- > visibleProjects <- query @Project+-- > |> filterWhereIn (#status, [Draft, Active])+-- > |> fetch+-- > -- SELECT * FROM projects WHERE status IN ('draft', 'active')+--+-- For negation use 'filterWhereNotIn'+--+filterWhereIn :: forall name table model value queryBuilderProvider (joinRegister :: Type). (KnownSymbol table, KnownSymbol name, DefaultParamEncoder [value], DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, EqOrIsOperator value, Table model) => (Proxy name, [value]) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereIn (name, value) queryBuilderProvider =+ case head nullValues of+ Nothing -> injectQueryBuilder $ addCondition inCondition qb -- All values non null+ Just nullValue ->+ let+ isNullCondition = ColumnCondition columnName IsOp (Literal "NULL") Nothing Nothing+ in+ case head nonNullValues of+ Just nonNullValue -> -- Some non null values, some null values+ injectQueryBuilder $ addCondition (OrCondition inCondition isNullCondition) qb+ Nothing -> injectQueryBuilder $ addCondition isNullCondition qb -- All values null+ where+ -- Only NOT NULL values can be compares inside the IN expression, NULL values have to be compares using a manual appended IS expression+ -- https://github.com/digitallyinduced/ihp/issues/906+ --+ (nonNullValues, nullValues) = value |> partition (\v -> toEqOrIsOperator v == EqOp)++ inCondition = ColumnCondition columnName InOp (paramValue nonNullValues) Nothing Nothing++ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+ qb = getQueryBuilder queryBuilderProvider+{-# INLINE filterWhereIn #-}++-- Like 'filterWhereIn', but case insensitive.+--+-- __Example:__ Only show users where @email@ is @User1@example.com@ or @User2@example.com@.+--+-- > users <- query @User+-- > |> filterWhereInCaseInsensitive (#email, ['User1@example.com', 'User2@example.com'])+-- > |> fetch+-- > -- SELECT * FROM users WHERE LOWER(email) IN ('user1@example.com', 'user2@example.com')+--+filterWhereInCaseInsensitive :: forall name table model value queryBuilderProvider (joinRegister :: Type). ( KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, EqOrIsOperator value, Table model) => (Proxy name, [Text]) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereInCaseInsensitive (name, values) queryBuilderProvider =+ case head nullValues of+ Nothing -> injectQueryBuilder $ addCondition inCondition qb+ Just nullValue ->+ let+ isNullCondition = ColumnCondition columnName IsOp (Literal "NULL") Nothing Nothing+ in+ case head nonNullValues of+ Just _ ->+ injectQueryBuilder $ addCondition (OrCondition inCondition isNullCondition) qb+ Nothing -> injectQueryBuilder $ addCondition isNullCondition qb+ where+ (nonNullValues, nullValues) = values |> partition (\v -> toEqOrIsOperator v == EqOp)++ lowerValues = map toLower nonNullValues++ inCondition = ColumnCondition lowerColumnName InOp (paramValue lowerValues) Nothing Nothing++ lowerColumnName = "LOWER(" <> columnName <> ")"+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+ qb = getQueryBuilder queryBuilderProvider++{-# INLINE filterWhereInCaseInsensitive #-}++-- | Like 'filterWhereIn', but takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.+--+-- __Example:__ get posts by Tom and Tim.+--+-- > tomOrTimPosts <- query @Post+-- > |> innerJoin @User (#createdBy, #id)+-- > |> filterWhereInJoinedTable @User (#name, ["Tom","Tim"])+-- > |> fetch+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name IN ('Tom', 'Tim')+--+filterWhereInJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder [value], HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, [value]) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereInJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName InOp (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereInJoinedTable #-}+++-- | Adds a @WHERE x NOT IN (y)@ condition to the query.+--+-- __Example:__ Only show projects where @status@ is not @Archived@+--+-- > visibleProjects <- query @Project+-- > |> filterWhereNotIn (#status, [Archived])+-- > |> fetch+-- > -- SELECT * FROM projects WHERE status NOT IN ('archived')+--+-- The inclusive version of this function is called 'filterWhereIn'.+--+filterWhereNotIn :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder [value], DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, EqOrIsOperator value) => (Proxy name, [value]) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereNotIn (_, []) queryBuilder = queryBuilder -- Handle empty case by ignoring query part: `WHERE x NOT IN ()`+filterWhereNotIn (name, value) queryBuilderProvider =+ case head nullValues of+ Nothing -> injectQueryBuilder $ addCondition notInCondition qb -- All values non null+ Just nullValue ->+ case head nonNullValues of+ Just nonNullValue -> injectQueryBuilder $ addCondition (ColumnCondition columnName IsNotOp (Literal "NULL") Nothing Nothing) (addCondition notInCondition qb) -- Some non null values, some null values+ Nothing -> injectQueryBuilder $ addCondition (ColumnCondition columnName IsNotOp (Literal "NULL") Nothing Nothing) qb -- All values null+ where+ -- Only NOT NULL values can be compares inside the IN expression, NULL values have to be compares using a manual appended IS expression+ -- https://github.com/digitallyinduced/ihp/issues/906+ --+ (nonNullValues, nullValues) = value |> partition (\v -> toEqOrIsOperator v == EqOp)++ notInCondition = ColumnCondition columnName NotInOp (paramValue nonNullValues) Nothing Nothing++ columnName = qualifiedColumnName (symbolToText @table) (symbolToText @name)+ qb = getQueryBuilder queryBuilderProvider+{-# INLINE filterWhereNotIn #-}++-- | Like 'filterWhereNotIn', but takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.+--+-- __Example:__ get posts by users not named Tom or Tim.+--+-- > notTomOrTimPosts <- query @Post+-- > |> innerJoin @User (#createdBy, #id)+-- > |> filterWhereNotInJoinedTable @User (#name, ["Tom","Tim"])+-- > |> fetch+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name NOT IN ('Tom', 'Tim')+filterWhereNotInJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder [value], HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, [value]) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereNotInJoinedTable (_, []) queryBuilderProvider = queryBuilderProvider -- Handle empty case by ignoring query part: `WHERE x NOT IN ()`+filterWhereNotInJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName NotInOp (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereNotInJoinedTable #-}+++-- | Adds a @WHERE x LIKE y@ condition to the query.+--+-- __Example:__ Find titles matching search term.+--+-- > articles <- query @Article+-- > |> filterWhereLike (#title, "%" <> searchTerm <> "%")+-- > |> fetch+-- > -- SELECT * FROM articles WHERE title LIKE '%..%'+filterWhereLike :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereLike (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (LikeOp CaseSensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereLike #-}++-- | Like 'filterWhereLik'e, but takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.+--+-- __Example:__ Serach for Posts by users whose name contains "olaf" (case insensitive)+--+-- > olafPosts <- query @Post+-- > |> innerJoin @User (#createdBy, #id)+-- > |> filterWhereLikeJoinedTable @User (#name, "%Olaf%")+-- > |> fetch+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name LIKE '%Olaf%'+filterWhereLikeJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol name, KnownSymbol table, table ~ GetTableName model, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereLikeJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (LikeOp CaseSensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereLikeJoinedTable #-}+++-- | Adds a @WHERE x ILIKE y@ condition to the query. Case-insensitive version of 'filterWhereLike'.+--+-- __Example:__ Find titles matching search term.+--+-- > articles <- query @Article+-- > |> filterWhereILike (#title, "%" <> searchTerm <> "%")+-- > |> fetch+-- > -- SELECT * FROM articles WHERE title ILIKE '%..%'+filterWhereILike :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereILike (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (LikeOp CaseInsensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereILike #-}++-- | Like 'filterWhereILike'; case-insensitive version of filterWhereLikeJoinedTable, takes a type argument specifying the table which holds the column that is compared. The table needs to have been joined before using 'innerJoin' or 'innerJoinThirdTable'.+--+-- __Example:__ Serach for Posts by users whose name contains "olaf" (case insensitive)+--+-- > olafPosts <-+-- > query @Post+-- |> innerJoin @User (#createdBy, #id)+-- |> filterWhereILikeJoinedTable @User (#name, "%Olaf%")+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name ILIKE '%Olaf%'+filterWhereILikeJoinedTable :: forall model table name table' model' value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, table ~ GetTableName model, model' ~ GetModelByTableName table', HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereILikeJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (LikeOp CaseInsensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereILikeJoinedTable #-}+++-- | Adds a @WHERE x ~ y@ condition to the query.+--+-- __Example:__ Find names with titles in front.+--+-- > articles <- query @User+-- > |> filterWhereMatches (#name, "^(M(rs|r|iss)|Dr|Sir). ")+-- > |> fetch+-- > -- SELECT * FROM articles WHERE title ~ '^(M(rs|r|iss)|Dr|Sir). '+filterWhereMatches :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereMatches (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (MatchesOp CaseSensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereMatches #-}++-- | Adds a @WHERE x ~ y@ condition to the query, where the column x is held by a joined table.+--+-- __Example:__ Find Posts by people with names with titles in front.+--+-- > articles <- query @Post+-- > |> innerJoin @User (#createdBy, #id)+-- > |> filterWhereMatchesJoinedTable (#title, "^(M(rs|r|iss|s)|Dr|Sir). ")+-- > |> fetch+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.title ~ '^(M(rs|r|iss|s)|Dr|Sir). '++filterWhereMatchesJoinedTable :: forall model table name value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereMatchesJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (MatchesOp CaseSensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereMatchesJoinedTable #-}+++-- | Adds a @WHERE x ~* y@ condition to the query. Case-insensitive version of 'filterWhereMatches'.+filterWhereIMatches :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereIMatches (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (MatchesOp CaseInsensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereIMatches #-}++-- | Case-insensitive version of 'filterWhereMatchesJoinedTable'+filterWhereIMatchesJoinedTable :: forall model table name value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table' -> queryBuilderProvider table'+filterWhereIMatchesJoinedTable (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName (MatchesOp CaseInsensitive) (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereIMatchesJoinedTable #-}+++-- | Filter all rows by whether a field is in the past, determined by comparing 'NOW()' to the field's value.+--+-- Opposite of 'filterWhereFuture'+--+-- __Example:__ Fetch all posts scheduled for the past.+--+-- > publicPosts <- query @Post+-- > |> filterWherePast #scheduledAt+-- > |> fetch+-- > -- SELECT * FROM posts WHERE scheduled_at <= NOW()+filterWherePast+ :: ( KnownSymbol table+ , KnownSymbol name+ , HasField name (GetModelByTableName table) value+ , HasQueryBuilder queryBuilderProvider joinRegister+ , Table (GetModelByTableName table)+ )+ => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table+filterWherePast name = filterWhereSql (name, "<= NOW()")+{-# INLINE filterWherePast #-}++-- | Filter all rows by whether a field is in the future, determined by comparing 'NOW()' to the field's value.+--+-- Opposite of 'filterWherePast'+--+-- __Example:__ Fetch all posts scheduled for the future.+--+-- > hiddenPosts <- query @Post+-- > |> filterWhereFuture #scheduledAt+-- > |> fetch+-- > -- SELECT * FROM posts WHERE scheduled_at > NOW()+filterWhereFuture+ :: ( KnownSymbol table+ , KnownSymbol name+ , HasField name (GetModelByTableName table) value+ , HasQueryBuilder queryBuilderProvider joinRegister+ , Table (GetModelByTableName table)+ )+ => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereFuture name = filterWhereSql (name, "> NOW()")+{-# INLINE filterWhereFuture #-}++-- | Adds a @WHERE x > y@ condition to the query.+--+-- __Example:__ Find assignments with grade greater than 80.+--+-- > greatAssignments <- query @Assignment+-- > |> filterWhereGreaterThan (#grade, 80)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade > 80+--+-- See also: 'filterWhereLarger', 'filterWhereGreaterThanOrEqualTo', 'filterWhereAtLeast'+filterWhereGreaterThan :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereGreaterThan (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName GreaterThanOp (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereGreaterThan #-}++-- | Alias for 'filterWhereGreaterThan'. Adds a @WHERE x > y@ condition to the query.+--+-- __Example:__ Find assignments with grade larger than 80.+--+-- > greatAssignments <- query @Assignment+-- > |> filterWhereLarger (#grade, 80)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade > 80+filterWhereLarger :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereLarger = filterWhereGreaterThan+{-# INLINE filterWhereLarger #-}++-- | Adds a @WHERE x >= y@ condition to the query.+--+-- __Example:__ Find assignments with grade at least 80.+--+-- > greatAssignments <- query @Assignment+-- > |> filterWhereGreaterThanOrEqualTo (#grade, 80)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade >= 80+--+-- See also: 'filterWhereAtLeast', 'filterWhereGreaterThan', 'filterWhereLarger'+filterWhereGreaterThanOrEqualTo :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereGreaterThanOrEqualTo (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName GreaterThanOrEqualToOp (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereGreaterThanOrEqualTo #-}++-- | Alias for 'filterWhereGreaterThanOrEqualTo'. Adds a @WHERE x >= y@ condition to the query.+--+-- __Example:__ Find assignments with grade at least 80.+--+-- > greatAssignments <- query @Assignment+-- > |> filterWhereAtLeast (#grade, 80)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade >= 80+filterWhereAtLeast :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereAtLeast = filterWhereGreaterThanOrEqualTo+{-# INLINE filterWhereAtLeast #-}++-- | Adds a @WHERE x < y@ condition to the query.+--+-- __Example:__ Find assignments with grade less than 60.+--+-- > poorAssignments <- query @Assignment+-- > |> filterWhereLessThan (#grade, 60)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade < 60+--+-- See also: 'filterWhereSmaller', 'filterWhereLessThanOrEqualTo', 'filterWhereAtMost'+filterWhereLessThan :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereLessThan (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName LessThanOp (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereLessThan #-}++-- | Alias for 'filterWhereLessThan'. Adds a @WHERE x < y@ condition to the query.+--+-- __Example:__ Find assignments with grade smaller than 60.+--+-- > poorAssignments <- query @Assignment+-- > |> filterWhereSmaller (#grade, 60)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade < 60+filterWhereSmaller :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereSmaller = filterWhereLessThan+{-# INLINE filterWhereSmaller #-}++-- | Adds a @WHERE x <= y@ condition to the query.+--+-- __Example:__ Find assignments with grade at most 60.+--+-- > poorAssignments <- query @Assignment+-- > |> filterWhereLessThanOrEqualTo (#grade, 60)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade <= 60+--+-- See also: 'filterWhereAtMost', 'filterWhereLessThan', 'filterWhereSmaller'+filterWhereLessThanOrEqualTo :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereLessThanOrEqualTo (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName LessThanOrEqualToOp (paramValue value) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereLessThanOrEqualTo #-}++-- | Alias for 'filterWhereLessThanOrEqualTo'. Adds a @WHERE x <= y@ condition to the query.+--+-- __Example:__ Find assignments with grade at most 60.+--+-- > poorAssignments <- query @Assignment+-- > |> filterWhereAtMost (#grade, 60)+-- > |> fetch+-- > -- SELECT * FROM assignments WHERE grade <= 60+filterWhereAtMost :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereAtMost = filterWhereLessThanOrEqualTo+{-# INLINE filterWhereAtMost #-}+++-- | Allows to add a custom raw sql where condition+--+-- If your query cannot be represented with 'filterWhereSql', take a look at 'IHP.ModelSupport.sqlQuery'.+--+-- __Example:__ Fetching all projects created in the last 24 hours.+--+-- > latestProjects <- query @Project+-- > |> filterWhereSql (#startedAt, "< current_timestamp - interval '1 day'")+-- > |> fetch+-- > -- SELECT * FROM projects WHERE started_at < current_timestamp - interval '1 day'+--+filterWhereSql :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, Text) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereSql (name, sqlCondition) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = ColumnCondition columnName SqlOp (Literal sqlCondition) Nothing Nothing+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereSql #-}++-- | Adds a @WHERE LOWER(x) = LOWER(y)@ condition to the query.+--+-- __Example:__ Get a user by an email address, ignoring case+--+-- > user <- query @User+-- > |> filterWhereCaseInsensitive (#email, "marc@digitallyinduced.com")+-- > |> fetchOne+-- > -- SELECT * FROM users WHERE LOWER(email) = 'marc@digitallyinduced.com'+--+-- For high performance it's best to have an index for @LOWER(field)@ in your Schema.sql+--+-- >>> CREATE UNIQUE INDEX users_email_index ON users ((LOWER(email)));+--+filterWhereCaseInsensitive :: forall name table model value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, DefaultParamEncoder value, HasField name model value, EqOrIsOperator value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => (Proxy name, value) -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereCaseInsensitive (name, value) queryBuilderProvider = injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+ where+ condition = let op = toEqOrIsOperator value in ColumnCondition columnName op (condValueForOp op value) (Just "LOWER") (Just "LOWER")+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE filterWhereCaseInsensitive #-}+++filterWhereIdIn :: forall table model queryBuilderProvider (joinRegister :: Type). (KnownSymbol table, Table model, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, DefaultParamEncoder [PrimaryKey (GetTableName model)]) => [Id model] -> queryBuilderProvider table -> queryBuilderProvider table+filterWhereIdIn values queryBuilderProvider =+ -- We don't need to treat null values differently here, because primary keys imply not-null+ -- Extract the raw primary key values from the Id wrappers+ let+ rawPrimaryKeys = map (\(Id pk) -> pk) values+ condition = ColumnCondition (primaryKeyConditionColumnSelector @model) InOp (paramValue rawPrimaryKeys) Nothing Nothing+ in+ injectQueryBuilder $ addCondition condition (getQueryBuilder queryBuilderProvider)+{-# INLINE filterWhereIdIn #-}
@@ -0,0 +1,180 @@+{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}++{-|+Module: IHP.QueryBuilder.HasqlCompiler+Description: Compile QueryBuilder to Hasql Statement+Copyright: (c) digitally induced GmbH, 2025++This module compiles QueryBuilder queries directly to Hasql 'Statement' values+by threading a parameter counter and encoder accumulator through compilation.+-}+module IHP.QueryBuilder.HasqlCompiler+( buildStatement+, buildWrappedStatement+, toSQL+, compileOperator+, CompilerState(..)+, emptyCompilerState+, nextParam+) where++import IHP.Prelude+import qualified Hasql.Encoders as Encoders+import qualified Hasql.Decoders as Decoders+import qualified Hasql.Statement as Hasql+import Data.Functor.Contravariant (contramap)+import Data.Functor.Contravariant.Divisible (conquer)+import IHP.QueryBuilder.Types+import IHP.QueryBuilder.Compiler (buildQuery)+import qualified Data.List as List++-- | Compile context: parameter counter + accumulated encoder.+data CompilerState = CompilerState !Int !(Encoders.Params ())++-- | Initial compile context: counter starts at 1, no params.+emptyCompilerState :: CompilerState+emptyCompilerState = CompilerState 1 conquer+{-# INLINE emptyCompilerState #-}++-- | Assign the next @$N@ placeholder and accumulate the encoder.+nextParam :: Encoders.Params () -> CompilerState -> (Text, CompilerState)+nextParam enc (CompilerState n acc) = ("$" <> tshow n, CompilerState (n + 1) (acc <> enc))+{-# INLINE nextParam #-}++-- | Build a Hasql 'Statement' from a compiled 'SQLQuery' and a result decoder.+buildStatement :: SQLQuery -> Decoders.Result a -> Hasql.Statement () a+buildStatement sqlQuery decoder =+ let (sql, CompilerState _ encoder) = compileQuery emptyCompilerState sqlQuery+ in Hasql.preparable sql encoder decoder++-- | Like 'buildStatement', but wraps the compiled SQL with a prefix and suffix.+-- Used for @SELECT COUNT(*) FROM (inner) AS alias@ patterns.+buildWrappedStatement :: Text -> SQLQuery -> Text -> Decoders.Result a -> Hasql.Statement () a+buildWrappedStatement prefix sqlQuery suffix decoder =+ let (innerSql, CompilerState _ encoder) = compileQuery emptyCompilerState sqlQuery+ in Hasql.preparable (prefix <> innerSql <> suffix) encoder decoder++-- | Compile a QueryBuilder to SQL text (for testing / error messages).+-- Discards the encoder.+toSQL :: forall table queryBuilderProvider joinRegister. (KnownSymbol table, HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> Text+toSQL queryBuilderProvider =+ let (sql, _) = compileQuery emptyCompilerState (buildQuery queryBuilderProvider)+ in sql++-- | Compile a full SQLQuery to SQL text + updated compile context.+--+-- Structured so that the Nothing/empty branches contribute no concatenation;+-- GHC can see through the case alternatives and eliminate dead appends.+compileQuery :: CompilerState -> SQLQuery -> (Text, CompilerState)+compileQuery cc0 sqlQuery@SQLQuery { queryIndex, selectFrom, distinctClause, distinctOnClause, orderByClause, limitClause, offsetClause, columnsSql } =+ let -- Build the fixed prefix: SELECT [DISTINCT] [DISTINCT ON (...)] cols FROM table [JOINs]+ selectPart = case distinctClause of+ True -> case distinctOnClause of+ Just col -> "SELECT DISTINCT DISTINCT ON (" <> col <> ") " <> selectorsPart <> " FROM " <> selectFrom+ Nothing -> "SELECT DISTINCT " <> selectorsPart <> " FROM " <> selectFrom+ False -> case distinctOnClause of+ Just col -> "SELECT DISTINCT ON (" <> col <> ") " <> selectorsPart <> " FROM " <> selectFrom+ Nothing -> "SELECT " <> selectorsPart <> " FROM " <> selectFrom++ selectorsPart = case queryIndex of+ Just idx -> idx <> ", " <> columnsSql+ Nothing -> columnsSql++ -- Joins: non-recursive, pattern-match on the list to avoid a function call for []+ withJoins = case joins sqlQuery of+ [] -> selectPart+ js -> selectPart <> compileJoinList (reverse js)++ -- WHERE: only append when there is a condition+ (withWhere, cc1) = case whereCondition sqlQuery of+ Nothing -> (withJoins, cc0)+ Just condition ->+ let (condText, cc') = compileCondition cc0 condition+ in (withJoins <> " WHERE " <> condText, cc')++ -- ORDER BY: only append when there are clauses+ withOrderBy = case orderByClause of+ [] -> withWhere+ clauses -> withWhere <> " ORDER BY " <> compileOrderByClauses clauses++ -- LIMIT: only append when set+ (withLimit, cc2) = case limitClause of+ Nothing -> (withOrderBy, cc1)+ Just n ->+ let enc = contramap (const (fromIntegral n :: Int32)) (Encoders.param (Encoders.nonNullable Encoders.int4))+ (placeholder, cc') = nextParam enc cc1+ in (withOrderBy <> " LIMIT " <> placeholder, cc')++ -- OFFSET: only append when set+ (result, cc3) = case offsetClause of+ Nothing -> (withLimit, cc2)+ Just n ->+ let enc = contramap (const (fromIntegral n :: Int32)) (Encoders.param (Encoders.nonNullable Encoders.int4))+ (placeholder, cc') = nextParam enc cc2+ in (withLimit <> " OFFSET " <> placeholder, cc')++ in (result, cc3)++-- | Compile a non-empty list of joins. Not called for empty join lists —+-- the caller pattern-matches on @[]@ directly.+compileJoinList :: [Join] -> Text+compileJoinList [] = ""+compileJoinList (j:js) = " INNER JOIN " <> table j <> " ON " <> tableJoinColumn j <> " = " <> table j <> "." <> otherJoinColumn j <> compileJoinList js++compileCondition :: CompilerState -> Condition -> (Text, CompilerState)+compileCondition cc (ColumnCondition column operator value applyLeft applyRight) =+ let applyFn fn txt = case fn of+ Just f -> f <> "(" <> txt <> ")"+ Nothing -> txt+ colText = applyFn applyLeft column+ opText = compileOperator operator+ (valText, cc') = case operator of+ IsOp -> ("NULL", cc)+ IsNotOp -> ("NULL", cc)+ _ -> compileConditionValue cc value+ valWrapped = case operator of+ InOp -> "(" <> valText <> ")"+ NotInOp -> "(" <> valText <> ")"+ SqlOp -> valText+ _ -> applyFn applyRight valText+ in case operator of+ SqlOp -> (colText <> " " <> valWrapped, cc')+ _ -> (colText <> " " <> opText <> " " <> valWrapped, cc')+compileCondition cc (OrCondition a b) =+ let (aText, cc1) = compileCondition cc a+ (bText, cc2) = compileCondition cc1 b+ in ("(" <> aText <> ") OR (" <> bText <> ")", cc2)+compileCondition cc (AndCondition a b) =+ let (aText, cc1) = compileCondition cc a+ (bText, cc2) = compileCondition cc1 b+ in ("(" <> aText <> ") AND (" <> bText <> ")", cc2)++compileConditionValue :: CompilerState -> ConditionValue -> (Text, CompilerState)+compileConditionValue cc (Param enc) = nextParam enc cc+compileConditionValue cc (Literal t) = (t, cc)++compileOrderByClauses :: [OrderByClause] -> Text+compileOrderByClauses clauses = mconcat (List.intersperse "," (map compileOrderByClause clauses))+ where+ compileOrderByClause OrderByClause { orderByColumn, orderByDirection } =+ orderByColumn <> (if orderByDirection == Desc then " DESC" else "")++-- | Compiles a 'FilterOperator' to its SQL representation+compileOperator :: FilterOperator -> Text+compileOperator EqOp = "="+compileOperator NotEqOp = "!="+compileOperator InOp = "= ANY"+compileOperator NotInOp = "<> ALL"+compileOperator IsOp = "IS"+compileOperator IsNotOp = "IS NOT"+compileOperator (LikeOp CaseSensitive) = "LIKE"+compileOperator (LikeOp CaseInsensitive) = "ILIKE"+compileOperator (NotLikeOp CaseSensitive) = "NOT LIKE"+compileOperator (NotLikeOp CaseInsensitive) = "NOT ILIKE"+compileOperator (MatchesOp CaseSensitive) = "~"+compileOperator (MatchesOp CaseInsensitive) = "~*"+compileOperator GreaterThanOp = ">"+compileOperator GreaterThanOrEqualToOp = ">="+compileOperator LessThanOp = "<"+compileOperator LessThanOrEqualToOp = "<="+compileOperator SqlOp = ""
@@ -0,0 +1,37 @@+-- | Shared helpers for building dynamic hasql queries.+--+-- These are used by both @ihp-datasync@ ('IHP.DataSync.DynamicQuery') and+-- @ihp-ide@ ('IHP.IDE.Data.Controller') for executing dynamically-constructed+-- SQL through hasql.+module IHP.QueryBuilder.HasqlHelpers+ ( wrapDynamicQuery+ , quoteIdentifier+ ) where++import IHP.Prelude+import qualified Hasql.DynamicStatements.Snippet as Snippet+import Hasql.DynamicStatements.Snippet (Snippet)+import qualified Data.Text as Text++-- | Wraps a SQL query snippet so that each row is returned as a JSON object.+--+-- This is needed because hasql decoders are positional and don't provide column name metadata.+-- By wrapping with @row_to_json@, we get column names in the JSON keys, which we can then+-- decode into a list of fields.+--+-- Uses a CTE (Common Table Expression) which works for both SELECT queries+-- and DML statements (INSERT, UPDATE, DELETE) with RETURNING:+--+-- @+-- WITH _ihp_dynamic_result AS (...original query...) SELECT row_to_json(t)::jsonb FROM _ihp_dynamic_result AS t+-- @+wrapDynamicQuery :: Snippet -> Snippet+wrapDynamicQuery innerQuery =+ Snippet.sql "WITH _ihp_dynamic_result AS (" <> innerQuery <> Snippet.sql ") SELECT row_to_json(t)::jsonb FROM _ihp_dynamic_result AS t"++-- | Quote a SQL identifier (table name, column name) to prevent SQL injection.+--+-- Wraps the identifier in double quotes and escapes any embedded double quotes+-- by doubling them, following the SQL standard.+quoteIdentifier :: Text -> Snippet+quoteIdentifier name = Snippet.sql (cs ("\"" <> Text.replace "\"" "\"\"" name <> "\""))
@@ -0,0 +1,107 @@+{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}+{-|+Module: IHP.QueryBuilder.Join+Description: JOIN operations for QueryBuilder+Copyright: (c) digitally induced GmbH, 2020++This module provides functions for joining tables in queries.+-}+module IHP.QueryBuilder.Join+( innerJoin+, innerJoinThirdTable+, labelResults+) where++import IHP.Prelude+import IHP.ModelSupport+import IHP.QueryBuilder.Types+import IHP.QueryBuilder.Compiler (qualifiedColumnName)++-- | Joins a table to an existing QueryBuilder (or something holding a QueryBuilder) on the specified columns. Example:+-- > query @Posts+-- > |> innerJoin @Users (#author, #id)+-- > -- SELECT users.* FROM users INNER JOIN posts ON users.id = posts.author ...+innerJoin :: forall model' table' name' value' model table name value queryBuilderProvider joinRegister.+ (+ KnownSymbol name,+ KnownSymbol table,+ HasField name model value,+ KnownSymbol name',+ KnownSymbol table',+ HasQueryBuilder queryBuilderProvider joinRegister,+ ModelList joinRegister,+ HasField name' model' value',+ value ~ value',+ model ~ GetModelByTableName table,+ table' ~ GetTableName model'+ ) => (Proxy name, Proxy name') -> queryBuilderProvider table -> JoinQueryBuilderWrapper (ConsModelList model' joinRegister) table+innerJoin (name, name') queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ joinInfo = Join joinTableName leftJoinColumn rightJoinColumn+ in injectQueryBuilder $ QueryBuilder sq { joins = joinInfo : joins sq }+ where+ baseTableName = symbolToText @table+ joinTableName = symbolToText @table'+ leftJoinColumn = qualifiedColumnName baseTableName (symbolToText @name)+ rightJoinColumn = fieldNameToColumnName (symbolToText @name')+{-# INLINE innerJoin #-}++-- | Index the values from a table with values of a field from a table joined by 'innerJoin' or 'innerJoinThirdTable'. Useful to get, e.g., the tags to a set of posts in such a way that the assignment of tags to posts is preserved.+--+--+-- __Example:__ Fetch a list of all comments, each paired with the id of the post it belongs to.+--+-- > labeledTags <-+-- > query @Tag+-- > |> innerJoin @Tagging (#id, #tagId)+-- > |> innerJoinThirdTable @Post @Tagging (#id, #postId)+-- > |> labelResults @Post #id+-- > |> fetch+-- > -- SELECT posts.id, tags.* FROM comments INNER JOIN taggings ON tags.id = taggings.tagId INNER JOIN posts ON posts.id = taggings.postId+--+-- labeledTags is then a list of type ['LabeledData' (Id' "posts") Tag] such that "LabeledData postId tag" is contained in that list if "tag" is a tag of the post with id postId.+--+labelResults :: forall foreignModel baseModel foreignTable baseTable name value queryBuilderProvider joinRegister.+ (+ KnownSymbol foreignTable,+ KnownSymbol baseTable,+ foreignTable ~ GetTableName foreignModel,+ baseModel ~ GetModelByTableName baseTable,+ HasField name foreignModel value,+ HasQueryBuilder queryBuilderProvider joinRegister,+ KnownSymbol name,+ IsJoined foreignModel joinRegister+ ) => Proxy name -> queryBuilderProvider baseTable -> LabeledQueryBuilderWrapper foreignTable name value baseTable+labelResults name queryBuilderProvider = LabeledQueryBuilderWrapper $ getQueryBuilder queryBuilderProvider++-- | Joins a table on a column held by a previously joined table. Example:+-- > query @Posts+-- > |> innerJoin @Users (#author, #id)+-- > |> innerJoinThirdTable @City @Users (#id, #homeTown)+-- > -- SELECT posts.* FROM posts INNER JOIN users ON posts.author = users.id INNER JOIN cities ON user.home_town = cities.id+--+innerJoinThirdTable :: forall model model' name name' value value' table table' baseTable baseModel queryBuilderProvider joinRegister.+ (+ KnownSymbol name,+ KnownSymbol table,+ HasField name model value,+ KnownSymbol name',+ KnownSymbol table',+ HasQueryBuilder queryBuilderProvider joinRegister,+ ModelList joinRegister,+ HasField name' model' value',+ value ~ value',+ table ~ GetTableName model,+ table' ~ GetTableName model',+ baseModel ~ GetModelByTableName baseTable+ ) => (Proxy name, Proxy name') -> queryBuilderProvider baseTable -> JoinQueryBuilderWrapper (ConsModelList model joinRegister) baseTable+innerJoinThirdTable (name, name') queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ joinInfo = Join joinTableName leftJoinColumn rightJoinColumn+ in injectQueryBuilder $ QueryBuilder sq { joins = joinInfo : joins sq }+ where+ baseTableName = symbolToText @table'+ joinTableName = symbolToText @table+ leftJoinColumn = qualifiedColumnName baseTableName (symbolToText @name')+ rightJoinColumn = fieldNameToColumnName (symbolToText @name)+{-# INLINE innerJoinThirdTable #-}
@@ -0,0 +1,179 @@+{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}+{-|+Module: IHP.QueryBuilder.Order+Description: ORDER BY, LIMIT, OFFSET, and DISTINCT operations for QueryBuilder+Copyright: (c) digitally induced GmbH, 2020++This module provides functions for ordering, limiting, and deduplicating query results.+-}+module IHP.QueryBuilder.Order+( orderBy+, orderByAsc+, orderByDesc+, orderByJoinedTable+, orderByAscJoinedTable+, orderByDescJoinedTable+, limit+, offset+, distinct+, distinctOn+) where++import IHP.Prelude+import IHP.ModelSupport+import IHP.QueryBuilder.Types+import IHP.QueryBuilder.Compiler (qualifiedColumnName)++-- | Adds an @ORDER BY .. ASC@ to your query.+--+-- Use 'orderByDesc' for descending order.+--+-- __Example:__ Fetch the 10 oldest books.+--+-- > query @Book+-- > |> orderBy #createdAt -- > |> limit 10+-- > |> fetch+-- > -- SELECT * FROM books LIMIT 10 ORDER BY created_at ASC+orderByAsc :: forall name model table value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table+orderByAsc !name queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { orderByClause = orderByClause sq <> [OrderByClause { orderByColumn = columnName, orderByDirection = Asc }] }+ where+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE orderByAsc #-}++-- | Adds an @ORDER BY .. ASC@ on a joined table column to your query.+--+-- Use 'orderByDescJoinedTable' for descending order.+--+-- __Example:__ Order joined `User` records by `username` ascending.+--+-- > query @Project+-- > |> innerJoin @User (#id, #projectId)+-- > |> orderByAscJoinedTable #username+-- > |> fetch+-- > -- SELECT ... FROM projects+-- > -- INNER JOIN users ON projects.id = users.project_id+-- > -- ORDER BY users.username ASC+orderByAscJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. ( KnownSymbol table, KnownSymbol name, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => Proxy name -> queryBuilderProvider table' -> queryBuilderProvider table'+orderByAscJoinedTable !name queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { orderByClause = orderByClause sq <> [OrderByClause { orderByColumn = columnName, orderByDirection = Asc }] }+ where+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE orderByAscJoinedTable #-}++-- | Adds an @ORDER BY .. DESC@ to your query.+--+-- Use 'orderBy' for ascending order.+--+-- __Example:__ Fetch the 10 newest projects (ordered by creation time).+--+-- > query @Project+-- > |> orderByDesc #createdAt+-- > |> limit 10+-- > |> fetch+-- > -- SELECT * FROM projects LIMIT 10 ORDER BY created_at DESC+orderByDesc :: forall name model table value queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table+orderByDesc !name queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { orderByClause = orderByClause sq <> [OrderByClause { orderByColumn = columnName, orderByDirection = Desc }] }+ where+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE orderByDesc #-}++-- | Adds an @ORDER BY .. DESC@ on a joined table column to your query.+--+-- Use 'orderByAscJoinedTable' for ascending order.+--+-- __Example:__ Order joined `User` records by `username` descending.+--+-- > query @Project+-- > |> innerJoin @User (#id, #projectId)+-- > |> orderByDescJoinedTable #username+-- > |> fetch+-- > -- SELECT ... FROM projects+-- > -- INNER JOIN users ON projects.id = users.project_id+-- > -- ORDER BY users.username DESC+orderByDescJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. ( KnownSymbol table, KnownSymbol name, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => Proxy name -> queryBuilderProvider table' -> queryBuilderProvider table'+orderByDescJoinedTable !name queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { orderByClause = orderByClause sq <> [OrderByClause { orderByColumn = columnName, orderByDirection = Desc }] }+ where+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE orderByDescJoinedTable #-}++-- | Alias for 'orderByAsc'+orderBy :: (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table+orderBy !name = orderByAsc name+{-# INLINE orderBy #-}++-- | Alias for 'orderByAscJoinedTable'+orderByJoinedTable :: forall model name table value queryBuilderProvider joinRegister table'. (KnownSymbol table, KnownSymbol name, HasField name model value, table ~ GetTableName model, HasQueryBuilder queryBuilderProvider joinRegister, IsJoined model joinRegister, Table model) => Proxy name -> queryBuilderProvider table' -> queryBuilderProvider table'+orderByJoinedTable !name = orderByAscJoinedTable @model @name @table @value @queryBuilderProvider @joinRegister @table' name+{-# INLINE orderByJoinedTable #-}++-- | Adds an @LIMIT ..@ to your query.+--+--+-- __Example:__ Fetch 10 posts+--+-- > query @Post+-- > |> limit 10+-- > |> fetch+-- > -- SELECT * FROM posts LIMIT 10+limit :: (HasQueryBuilder queryBuilderProvider joinRegister) => Int -> queryBuilderProvider model -> queryBuilderProvider model+limit !queryLimit queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { limitClause = Just queryLimit }+{-# INLINE limit #-}++-- | Adds an @OFFSET ..@ to your query. Most often used together with @LIMIT...@+--+--+-- __Example:__ Fetch posts 10-20+--+-- > query @Post+-- > |> limit 10+-- > |> offset 10+-- > |> fetch+-- > -- SELECT * FROM posts LIMIT 10 OFFSET 10+offset :: (HasQueryBuilder queryBuilderProvider joinRegister) => Int -> queryBuilderProvider model -> queryBuilderProvider model+offset !queryOffset queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { offsetClause = Just queryOffset }+{-# INLINE offset #-}++-- | Adds a @DISTINCT@ to your query.+--+-- Use 'distinct' to remove all duplicate rows from the result+--+-- __Example:__ Fetch distinct books+--+-- > query @Book+-- > |> distinct+-- > |> fetch+-- > -- SELECT DISTINCT * FROM books+distinct :: (HasQueryBuilder queryBuilderProvider joinRegister) => queryBuilderProvider table -> queryBuilderProvider table+distinct queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { distinctClause = True }+{-# INLINE distinct #-}++-- | Adds an @DISTINCT ON .. to your query.+--+-- Use 'distinctOn' to return a single row for each distinct value provided.+--+-- __Example:__ Fetch one book for each categoryId field+--+-- > query @Book+-- > |> distinctOn #categoryId+-- > |> fetch+-- > -- SELECT DISTINCT ON (category_id) * FROM books+distinctOn :: forall name model value table queryBuilderProvider joinRegister. (KnownSymbol table, KnownSymbol name, HasField name model value, model ~ GetModelByTableName table, HasQueryBuilder queryBuilderProvider joinRegister, Table model) => Proxy name -> queryBuilderProvider table -> queryBuilderProvider table+distinctOn !name queryBuilderProvider =+ let QueryBuilder sq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder sq { distinctOnClause = Just columnName }+ where+ columnName = qualifiedColumnName (tableName @model) (symbolToText @name)+{-# INLINE distinctOn #-}
@@ -0,0 +1,300 @@+{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}+{-|+Module: IHP.QueryBuilder.Types+Description: Core data types for the QueryBuilder+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.QueryBuilder.Types+( -- * Core Types+ QueryBuilder (..)+, SQLQuery (..)+, Condition (..)+, ConditionValue (..)+, Join (..)+, OrderByClause (..)+, OrderByDirection (..)+, FilterOperator (..)+, MatchSensitivity (..)+ -- * Helpers+, addCondition+, qualifyAndJoinColumns+ -- * Type-level Join Tracking+, NoJoins+, EmptyModelList+, ConsModelList+, ModelList+, IsJoined+ -- * QueryBuilder Wrappers+, JoinQueryBuilderWrapper (..)+, NoJoinQueryBuilderWrapper (..)+, LabeledQueryBuilderWrapper (..)+ -- * Type Classes+, HasQueryBuilder (..)+, DefaultScope (..)+, EqOrIsOperator (..)+, FilterPrimaryKey (..)+) where++import IHP.Prelude+import IHP.ModelSupport+import IHP.HSX.ToHtml+import qualified Control.DeepSeq as DeepSeq+import qualified GHC.Generics+import qualified Hasql.Encoders as Encoders+import qualified Prelude+import qualified Data.Text as Text++-- | Represents whether string matching should be case-sensitive or not+data MatchSensitivity = CaseSensitive | CaseInsensitive deriving (Show, Eq)++-- | Operators used in WHERE clause conditions+data FilterOperator+ = EqOp -- ^ @col = val@+ | NotEqOp -- ^ @col != val@+ | InOp -- ^ @col IN (set)@+ | NotInOp -- ^ @col NOT IN (set)@+ | IsOp -- ^ @col IS val@+ | IsNotOp -- ^ @col IS NOT val@+ | LikeOp !MatchSensitivity -- ^ @col LIKE val@+ | NotLikeOp !MatchSensitivity -- ^ @col NOT LIKE val@+ | MatchesOp !MatchSensitivity -- ^ @col ~ pattern@+ | GreaterThanOp -- ^ @col > val@+ | GreaterThanOrEqualToOp -- ^ @col >= val@+ | LessThanOp -- ^ @col < val@+ | LessThanOrEqualToOp -- ^ @col <= val@+ | SqlOp -- ^ Used by 'filterWhereSql'+ deriving (Show, Eq)++-- | Represents an ORDER BY clause component+data OrderByClause =+ OrderByClause+ { orderByColumn :: !Text+ , orderByDirection :: !OrderByDirection }+ deriving (Show, Eq, GHC.Generics.Generic, DeepSeq.NFData)++-- | Type-level marker indicating no joins are allowed+data NoJoins++-- | Type-level empty list for tracking joined tables+data EmptyModelList++-- | Type-level cons cell for tracking joined tables+data ConsModelList model models++-- | Type class to represent the true list type EmptyModelList ConsModelList.+class ModelList a++instance ModelList EmptyModelList+instance ModelList (ConsModelList model models)++-- | Type class to query containment in the type-level list.+class IsJoined a b++instance (ModelList b) => IsJoined a (ConsModelList a b)+instance {-# OVERLAPPABLE #-} (ModelList b, IsJoined a b) => IsJoined a (ConsModelList c b)++-- | Class to generalise over different QueryBuilder-providing types. The actual query builder can be extracted with 'getQueryBuilder' and injected with 'injectQueryBuilder'. Also assigns a join register to a queryBuilderProvider.+class HasQueryBuilder queryBuilderProvider joinRegister | queryBuilderProvider -> joinRegister where+ getQueryBuilder :: queryBuilderProvider table -> QueryBuilder table+ injectQueryBuilder :: QueryBuilder table -> queryBuilderProvider table+ getQueryIndex :: queryBuilderProvider table -> Maybe Text+ getQueryIndex _ = Nothing+ {-# INLINABLE getQueryIndex #-}++-- | Wrapper for QueryBuilders resulting from joins. Associates a joinRegister type.+newtype JoinQueryBuilderWrapper joinRegister table = JoinQueryBuilderWrapper (QueryBuilder table)++-- | Wrapper for QueryBuilder that must not joins, e.g. queryUnion.+newtype NoJoinQueryBuilderWrapper table = NoJoinQueryBuilderWrapper (QueryBuilder table)++-- | Wrapper for QueryBuilders with indexed results.+newtype LabeledQueryBuilderWrapper foreignTable indexColumn indexValue table = LabeledQueryBuilderWrapper (QueryBuilder table)++-- | QueryBuilders have query builders and the join register is empty.+instance HasQueryBuilder QueryBuilder EmptyModelList where+ getQueryBuilder = id+ {-# INLINE getQueryBuilder #-}+ injectQueryBuilder = id+ {-# INLINE injectQueryBuilder #-}++-- | JoinQueryBuilderWrappers have query builders+instance HasQueryBuilder (JoinQueryBuilderWrapper joinRegister) joinRegister where+ getQueryBuilder (JoinQueryBuilderWrapper queryBuilder) = queryBuilder+ {-# INLINABLE getQueryBuilder #-}+ injectQueryBuilder = JoinQueryBuilderWrapper+ {-# INLINABLE injectQueryBuilder #-}++-- | NoJoinQueryBuilderWrapper have query builders and the join register does not allow any joins+instance HasQueryBuilder NoJoinQueryBuilderWrapper NoJoins where+ getQueryBuilder (NoJoinQueryBuilderWrapper queryBuilder) = queryBuilder+ {-# INLINABLE getQueryBuilder #-}+ injectQueryBuilder = NoJoinQueryBuilderWrapper+ {-# INLINABLE injectQueryBuilder #-}++instance (KnownSymbol foreignTable, foreignModel ~ GetModelByTableName foreignTable , KnownSymbol indexColumn, HasField indexColumn foreignModel indexValue) => HasQueryBuilder (LabeledQueryBuilderWrapper foreignTable indexColumn indexValue) NoJoins where+ getQueryBuilder (LabeledQueryBuilderWrapper queryBuilder) = queryBuilder+ {-# INLINABLE getQueryBuilder #-}+ injectQueryBuilder = LabeledQueryBuilderWrapper+ {-# INLINABLE injectQueryBuilder #-}+ getQueryIndex _ = Just $ symbolToText @foreignTable <> "." <> fieldNameToColumnName (symbolToText @indexColumn)+ {-# INLINABLE getQueryIndex #-}++-- | The QueryBuilder is a flat newtype over SQLQuery. Each combinator directly+-- modifies fields of the underlying SQLQuery, avoiding any recursive tree traversal.+newtype QueryBuilder (table :: Symbol) = QueryBuilder { unQueryBuilder :: SQLQuery }++-- | Add a WHERE condition to a QueryBuilder, ANDing with any existing condition.+addCondition :: Condition -> QueryBuilder table -> QueryBuilder table+addCondition condition (QueryBuilder sq) = QueryBuilder $ sq+ { whereCondition = case whereCondition sq of+ Nothing -> Just condition+ Just existing -> Just (AndCondition existing condition)+ }+{-# INLINE addCondition #-}++-- | A condition value: either a parameterized encoder or a literal SQL fragment.+data ConditionValue+ = Param !(Encoders.Params ()) -- ^ Parameterized value: compiler assigns $N+ | Literal !Text -- ^ Raw SQL text (for filterWhereSql, NULL comparisons, etc.)++instance Show ConditionValue where+ showsPrec _ (Param _) = Prelude.showString "<Param>"+ showsPrec _ (Literal t) = Prelude.showString "Literal " . Prelude.shows t++instance Eq ConditionValue where+ Literal a == Literal b = a == b+ _ == _ = False -- Params cannot be compared for equality++-- | Represents a WHERE condition+data Condition+ = ColumnCondition !Text !FilterOperator !ConditionValue !(Maybe Text) !(Maybe Text)+ -- ^col ^op ^value ^applyLeft ^applyRight+ | OrCondition !Condition !Condition+ | AndCondition !Condition !Condition++-- | Returns a numeric tag for each Condition constructor.+-- Pattern match is exhaustive so adding a constructor triggers -Wincomplete-patterns.+conditionTag :: Condition -> Int+conditionTag ColumnCondition {} = 0+conditionTag OrCondition {} = 1+conditionTag AndCondition {} = 2++instance Eq Condition where+ (ColumnCondition c1 o1 s1 al1 ar1) == (ColumnCondition c2 o2 s2 al2 ar2) = c1 == c2 && o1 == o2 && s1 == s2 && al1 == al2 && ar1 == ar2+ (OrCondition l1 r1) == (OrCondition l2 r2) = l1 == l2 && r1 == r2+ (AndCondition l1 r1) == (AndCondition l2 r2) = l1 == l2 && r1 == r2+ a == b = conditionTag a == conditionTag b++deriving instance Show Condition++instance Show (QueryBuilder table) where+ show (QueryBuilder sq) = "QueryBuilder " ++ Prelude.show sq++instance Eq (QueryBuilder table) where+ (QueryBuilder sq1) == (QueryBuilder sq2) = sqlQueryEq sq1 sq2++-- | Compare two SQLQuery values. Uses snippetEq for comparing Condition fields.+sqlQueryEq :: SQLQuery -> SQLQuery -> Bool+sqlQueryEq a b =+ selectFrom a == selectFrom b+ && columnsSql a == columnsSql b+ && distinctClause a == distinctClause b+ && distinctOnClause a == distinctOnClause b+ && condEq (whereCondition a) (whereCondition b)+ && joins a == joins b+ && orderByClause a == orderByClause b+ && limitClause a == limitClause b+ && offsetClause a == offsetClause b+ where+ condEq Nothing Nothing = True+ condEq (Just c1) (Just c2) = c1 == c2+ condEq _ _ = False++-- | Display QueryBuilder's as their sql query inside HSX+instance KnownSymbol table => ToHtml (QueryBuilder table) where+ toHtml queryBuilder = toHtml (toSQLQueryBuilder queryBuilder)+ where+ -- Inline SQL generation for ToHtml to avoid circular imports+ toSQLQueryBuilder :: QueryBuilder table -> Text+ toSQLQueryBuilder qb = "QueryBuilder<" <> symbolToText @table <> ">"++-- | Represents a JOIN clause+data Join = Join { table :: Text, tableJoinColumn :: Text, otherJoinColumn :: Text }+ deriving (Show, Eq)++-- | ORDER BY direction+data OrderByDirection = Asc | Desc deriving (Eq, Show, GHC.Generics.Generic, DeepSeq.NFData)++-- | Represents a complete SQL query after building+data SQLQuery = SQLQuery+ { queryIndex :: !(Maybe Text)+ , selectFrom :: !Text+ , distinctClause :: !Bool+ , distinctOnClause :: !(Maybe Text)+ , whereCondition :: !(Maybe Condition)+ , joins :: ![Join]+ , orderByClause :: ![OrderByClause]+ , limitClause :: !(Maybe Int)+ , offsetClause :: !(Maybe Int)+ , columns :: ![Text]+ , columnsSql :: !Text+ -- ^ Pre-computed qualified column selector, e.g. @"users.id, users.name"@.+ -- Built once at query construction time so the compiler avoids re-qualifying+ -- and re-joining the column list on every compilation.+ } deriving (Show)+++instance SetField "queryIndex" SQLQuery (Maybe Text) where setField value sqlQuery = sqlQuery { queryIndex = value }+instance SetField "selectFrom" SQLQuery Text where setField value sqlQuery = sqlQuery { selectFrom = value }+instance SetField "distinctClause" SQLQuery Bool where setField value sqlQuery = sqlQuery { distinctClause = value }+instance SetField "distinctOnClause" SQLQuery (Maybe Text) where setField value sqlQuery = sqlQuery { distinctOnClause = value }+instance SetField "whereCondition" SQLQuery (Maybe Condition) where setField value sqlQuery = sqlQuery { whereCondition = value }+instance SetField "orderByClause" SQLQuery [OrderByClause] where setField value sqlQuery = sqlQuery { orderByClause = value }+instance SetField "limitClause" SQLQuery (Maybe Int) where setField value sqlQuery = sqlQuery { limitClause = value }+instance SetField "offsetClause" SQLQuery (Maybe Int) where setField value sqlQuery = sqlQuery { offsetClause = value }++-- | Type class for default scoping of queries+class DefaultScope table where+ defaultScope :: QueryBuilder table -> QueryBuilder table++instance {-# OVERLAPPABLE #-} DefaultScope table where+ {-# INLINE defaultScope #-}+ defaultScope queryBuilder = queryBuilder++instance Table (GetModelByTableName table) => Default (QueryBuilder table) where+ {-# INLINE def #-}+ def = let tn = tableName @(GetModelByTableName table)+ cols = columnNames @(GetModelByTableName table)+ in QueryBuilder SQLQuery+ { queryIndex = Nothing+ , selectFrom = tn+ , columns = cols+ , columnsSql = qualifyAndJoinColumns tn cols+ , distinctClause = False+ , distinctOnClause = Nothing+ , whereCondition = Nothing+ , joins = []+ , orderByClause = []+ , limitClause = Nothing+ , offsetClause = Nothing+ }++-- | Pre-compute the qualified, comma-separated column selector.+-- E.g. @qualifyAndJoinColumns "users" ["id", "name"] = "users.id, users.name"@+--+-- Intentionally NOINLINE: call sites (query, def) are lifted to CAFs, so+-- this is evaluated once per table type. NOINLINE prevents GHC from inlining+-- the map/intercalate into every use site of the resulting SQLQuery.+qualifyAndJoinColumns :: Text -> [Text] -> Text+qualifyAndJoinColumns tableName columns =+ Text.intercalate ", " (map (\c -> tableName <> "." <> c) columns)+{-# NOINLINE qualifyAndJoinColumns #-}++-- | Helper to deal with @some_field IS NULL@ and @some_field = 'some value'@+class EqOrIsOperator value where toEqOrIsOperator :: value -> FilterOperator+instance {-# OVERLAPS #-} EqOrIsOperator (Maybe something) where toEqOrIsOperator Nothing = IsOp; toEqOrIsOperator (Just _) = EqOp+instance {-# OVERLAPPABLE #-} EqOrIsOperator otherwise where toEqOrIsOperator _ = EqOp++-- | Type class for filtering by primary key+class FilterPrimaryKey table where+ filterWhereId :: Id' table -> QueryBuilder table -> QueryBuilder table
@@ -0,0 +1,96 @@+{-# LANGUAGE BangPatterns, TypeFamilies, DataKinds, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, StandaloneDeriving, FunctionalDependencies, FlexibleContexts, InstanceSigs, AllowAmbiguousTypes, DeriveAnyClass #-}+{-|+Module: IHP.QueryBuilder.Union+Description: UNION and OR operations for QueryBuilder+Copyright: (c) digitally induced GmbH, 2020++This module provides functions for combining queries with UNION and OR operations.+-}+module IHP.QueryBuilder.Union+( queryUnion+, queryUnionList+, queryOr+) where++import IHP.Prelude+import IHP.ModelSupport+import IHP.QueryBuilder.Types+import IHP.QueryBuilder.Compiler (query)++-- | Merges the results of two query builders by ORing their WHERE conditions.+--+-- Take a look at 'queryOr' as well, as this might be a bit shorter.+--+-- __Example:__ Return all pages owned by the user or owned by the users team.+--+-- > let userPages = query @Page |> filterWhere (#ownerId, currentUserId)+-- > let teamPages = query @Page |> filterWhere (#teamId, currentTeamId)+-- > pages <- queryUnion userPages teamPages |> fetch+-- > -- SELECT * FROM pages WHERE (owner_id = '..') OR (team_id = '..')+queryUnion :: (HasQueryBuilder queryBuilderProvider joinRegister, HasQueryBuilder r joinRegister') => queryBuilderProvider model -> r model -> NoJoinQueryBuilderWrapper model+queryUnion firstQueryBuilderProvider secondQueryBuilderProvider =+ let QueryBuilder first = getQueryBuilder firstQueryBuilderProvider+ QueryBuilder second = getQueryBuilder secondQueryBuilderProvider+ isSimple q = null (orderByClause q) && isNothing (limitClause q) && isNothing (offsetClause q) && null (joins q)+ unionWhere = case (whereCondition first, whereCondition second) of+ (Nothing, wc) -> wc+ (wc, Nothing) -> wc+ (Just a, Just b) -> Just (OrCondition a b)+ in if isSimple first && isSimple second+ then NoJoinQueryBuilderWrapper $ QueryBuilder first { whereCondition = unionWhere }+ else error "queryUnion: Union of complex queries (with ORDER BY, LIMIT, OFFSET, or JOINs) not supported"+{-# INLINE queryUnion #-}++-- | Like 'queryUnion', but applied on all the elements on the list+--+-- > action ProjectsAction = do+-- > let values :: [(ProjectType, Int)] = [(ProjectTypeOngoing, 3), (ProjectTypeNotStarted, 2)]+-- >+-- > valuePairToCondition :: (ProjectType, Int) -> QueryBuilder "projects"+-- > valuePairToCondition (projectType, participants) =+-- > query @Project+-- > |> filterWhere (#projectType, projectType)+-- > |> filterWhere (#participants, participants)+-- >+-- > theQuery = queryUnionList (map valuePairToCondition values)+-- >+-- > projects <- fetch theQuery+-- > render IndexView { .. }+queryUnionList :: forall table. (Table (GetModelByTableName table), KnownSymbol table, GetTableName (GetModelByTableName table) ~ table) => [QueryBuilder table] -> QueryBuilder table+-- For empty list, create a condition that is always false: id <> id (which is always false for non-null)+queryUnionList [] = addCondition (ColumnCondition "id" NotEqOp (Literal "id") Nothing Nothing) (query @(GetModelByTableName table) @table)+queryUnionList [single] = single+queryUnionList (first:rest) =+ let QueryBuilder firstSq = first+ QueryBuilder restSq = queryUnionList @table rest+ isSimple q = null (orderByClause q) && isNothing (limitClause q) && isNothing (offsetClause q) && null (joins q)+ unionWhere = case (whereCondition firstSq, whereCondition restSq) of+ (Nothing, wc) -> wc+ (wc, Nothing) -> wc+ (Just a, Just b) -> Just (OrCondition a b)+ in if isSimple firstSq && isSimple restSq+ then QueryBuilder firstSq { whereCondition = unionWhere }+ else error "queryUnionList: Union of complex queries (with ORDER BY, LIMIT, OFFSET, or JOINs) not supported"+++-- | Adds an @a OR b@ condition+--+-- __Example:__ Return all pages owned by the user or public.+--+-- > query @Page+-- > |> queryOr+-- > (filterWhere (#createdBy, currentUserId))+-- > (filterWhere (#public, True))+-- > |> fetch+-- > -- SELECT * FROM pages WHERE created_by = '..' OR public = True+queryOr :: (HasQueryBuilder queryBuilderProvider joinRegister, HasQueryBuilder queryBuilderProvider'' joinRegister'', HasQueryBuilder queryBuilderProvider''' joinRegister''') => (queryBuilderProvider model -> queryBuilderProvider''' model) -> (queryBuilderProvider model -> queryBuilderProvider'' model) -> queryBuilderProvider model -> queryBuilderProvider model+queryOr firstQuery secondQuery queryBuilderProvider =+ let QueryBuilder firstSq = getQueryBuilder $ firstQuery queryBuilderProvider+ QueryBuilder secondSq = getQueryBuilder $ secondQuery queryBuilderProvider+ unionWhere = case (whereCondition firstSq, whereCondition secondSq) of+ (Nothing, wc) -> wc+ (wc, Nothing) -> wc+ (Just a, Just b) -> Just (OrCondition a b)+ QueryBuilder baseSq = getQueryBuilder queryBuilderProvider+ in injectQueryBuilder $ QueryBuilder baseSq { whereCondition = unionWhere }+{-# INLINE queryOr #-}
@@ -0,0 +1,99 @@+{-# LANGUAGE TypeFamilies, DataKinds, MultiParamTypeClasses, PolyKinds, TypeApplications, ScopedTypeVariables, ConstraintKinds, TypeOperators, GADTs, UndecidableInstances, AllowAmbiguousTypes, FunctionalDependencies #-}++{-|+Module: IHP.Record+Description: Type-level record field operations+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.Record+( (|>)+, get+, set+, setJust+, setMaybe+, modify+, modifyJust+, SetField (..)+, UpdateField (..)+, incrementField+, decrementField+, CopyFields (..)+) where++import Data.Proxy+import GHC.TypeLits+import qualified GHC.Records as Record+import Prelude++-- | Pipe operator+infixl 8 |>+(|>) :: a -> (a -> b) -> b+a |> f = f a+{-# INLINE (|>) #-}++-- | Returns the field value for a field name+get :: forall model name value. (KnownSymbol name, Record.HasField name model value) => Proxy name -> model -> value+get _ record = Record.getField @name record+{-# INLINE get #-}++-- | Sets a field of a record and returns the new record.+set :: forall model name value. (KnownSymbol name, SetField name model value) => Proxy name -> value -> model -> model+set name value record = setField @name value record+{-# INLINE set #-}++-- | Like 'set' but doesn't set the value if it's 'Nothing'.+setMaybe :: forall model name value. (KnownSymbol name, SetField name model (Maybe value)) => Proxy name -> Maybe value -> model -> model+setMaybe name value record = case value of+ Just value -> setField @name (Just value) record+ Nothing -> record+{-# INLINE setMaybe #-}++-- | Like 'set' but wraps the value with a 'Just'.+setJust :: forall model name value. (KnownSymbol name, SetField name model (Maybe value)) => Proxy name -> value -> model -> model+setJust name value record = setField @name (Just value) record+{-# INLINE setJust #-}++modify :: forall model name value. (KnownSymbol name, Record.HasField name model value, SetField name model value) => Proxy name -> (value -> value) -> model -> model+modify _ updateFunction model = let value = Record.getField @name model in setField @name (updateFunction value) model+{-# INLINE modify #-}++-- | Like 'modify', but only modifies the value if it's not Nothing.+modifyJust :: forall model name value. (KnownSymbol name, Record.HasField name model (Maybe value), SetField name model (Maybe value)) => Proxy name -> (value -> value) -> model -> model+modifyJust _ updateFunction model = case Record.getField @name model of+ Just value -> setField @name (Just (updateFunction value)) model+ Nothing -> model+{-# INLINE modifyJust #-}++-- | Plus @1@ on record field.+incrementField :: forall model name value. (KnownSymbol name, Record.HasField name model value, SetField name model value, Num value) => Proxy name -> model -> model+incrementField _ model = let value = Record.getField @name model in setField @name (value + 1) model+{-# INLINE incrementField #-}++-- | Minus @1@ on a record field.+decrementField :: forall model name value. (KnownSymbol name, Record.HasField name model value, SetField name model value, Num value) => Proxy name -> model -> model+decrementField _ model = let value = Record.getField @name model in setField @name (value - 1) model+{-# INLINE decrementField #-}++class SetField (field :: GHC.TypeLits.Symbol) model value | field model -> value where+ setField :: value -> model -> model++class Record.HasField field model value => UpdateField (field :: GHC.TypeLits.Symbol) model model' value value' | model model' value' -> value where+ updateField :: value' -> model -> model'++class CopyFields (fields :: [Symbol]) destinationRecord sourceRecord where+ copyFields :: sourceRecord -> destinationRecord -> destinationRecord++instance CopyFields ('[]) destinationRecord sourceRecord where+ copyFields sourceRecord destinationRecord = destinationRecord+ {-# INLINE copyFields #-}++instance (CopyFields rest destinationRecord sourceRecord+ , KnownSymbol fieldName+ , SetField fieldName destinationRecord fieldType+ , Record.HasField fieldName sourceRecord fieldType+ ) => CopyFields (fieldName:rest) destinationRecord sourceRecord where+ copyFields sourceRecord destinationRecord =+ destinationRecord+ |> set (Proxy @fieldName) (Record.getField @fieldName sourceRecord)+ |> copyFields @rest sourceRecord+ {-# INLINE copyFields #-}
@@ -0,0 +1,57 @@+module IHP.RequestVault+( -- * Vault infrastructure (re-exported from Helper)+ module IHP.RequestVault.Helper+ -- * ModelContext (re-exported from ModelContext)+, module IHP.RequestVault.ModelContext+ -- * FrameworkConfig+, frameworkConfigVaultKey+, frameworkConfigMiddleware+, requestFrameworkConfig+ -- * PGListener+, pgListenerVaultKey+, pgListenerMiddleware+, requestPGListener+) where++import IHP.Prelude+import Network.Wai+import System.IO.Unsafe (unsafePerformIO)+import qualified Data.Vault.Lazy as Vault+import IHP.FrameworkConfig+import IHP.PGListener+import IHP.RequestVault.Helper+import IHP.RequestVault.ModelContext++-- request.frameworkConfig+frameworkConfigVaultKey :: Vault.Key FrameworkConfig+frameworkConfigVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE frameworkConfigVaultKey #-}++{-# INLINE frameworkConfigMiddleware #-}+frameworkConfigMiddleware :: FrameworkConfig -> Middleware+frameworkConfigMiddleware = insertVaultMiddleware frameworkConfigVaultKey++{-# INLINE requestFrameworkConfig #-}+requestFrameworkConfig :: Request -> FrameworkConfig+requestFrameworkConfig = lookupRequestVault frameworkConfigVaultKey++-- request.pgListener+pgListenerVaultKey :: Vault.Key PGListener+pgListenerVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE pgListenerVaultKey #-}++{-# INLINE pgListenerMiddleware #-}+pgListenerMiddleware :: PGListener -> Middleware+pgListenerMiddleware = insertVaultMiddleware pgListenerVaultKey++{-# INLINE requestPGListener #-}+requestPGListener :: Request -> PGListener+requestPGListener = lookupRequestVault pgListenerVaultKey++-- Field access helpers+instance HasField "frameworkConfig" Request FrameworkConfig where+ {-# INLINE getField #-}+ getField request = requestFrameworkConfig request+instance HasField "pgListener" Request PGListener where+ {-# INLINE getField #-}+ getField request = requestPGListener request
@@ -0,0 +1,40 @@+module IHP.RequestVault.Helper+( insertVaultMiddleware+, insertNewIORefVaultMiddleware+, lookupRequestVault+, insertVaultMiddlewareAndGetter+) where++import Prelude+import Data.IORef+import Network.Wai+import qualified Data.Vault.Lazy as Vault+import Data.Proxy+import Data.Typeable++{-# INLINE insertVaultMiddleware #-}+insertVaultMiddleware :: Vault.Key value -> value -> Middleware+insertVaultMiddleware key value app req respond = do+ let req' = req { vault = Vault.insert key value req.vault }+ app req' respond++{-# INLINE lookupRequestVault #-}+lookupRequestVault :: forall value. Typeable value => Vault.Key value -> Request -> value+lookupRequestVault key req =+ case Vault.lookup key req.vault of+ Just modelContext -> modelContext+ Nothing -> error $ "lookupRequestVault: Could not find " <> show (typeRep (Proxy @value) ) <> " in request.vault. Did you forget to add the middleware to your application?"++-- | Like 'insertVaultMiddleware', but creates a fresh 'IORef' with the given+-- default value on each request. Use this for mutable per-request state+-- (e.g. response headers, modal containers).+{-# INLINE insertNewIORefVaultMiddleware #-}+insertNewIORefVaultMiddleware :: Vault.Key (IORef value) -> value -> Middleware+insertNewIORefVaultMiddleware key defaultValue app req respond = do+ ref <- newIORef defaultValue+ let req' = req { vault = Vault.insert key ref req.vault }+ app req' respond++{-# INLINE insertVaultMiddlewareAndGetter #-}+insertVaultMiddlewareAndGetter :: Typeable value => Vault.Key value -> (value -> Middleware, Request -> value)+insertVaultMiddlewareAndGetter key = (insertVaultMiddleware key, lookupRequestVault key)
@@ -0,0 +1,43 @@+module IHP.RequestVault.ModelContext+( -- * ModelContext+ modelContextVaultKey+, modelContextMiddleware+, requestModelContext+ -- * RequestBody (re-exported from RequestBodyMiddleware)+, RequestBody (..)+, requestBodyVaultKey+) where++import GHC.Records (HasField(..))+import Network.Wai+import System.IO.Unsafe (unsafePerformIO)+import qualified Data.Vault.Lazy as Vault+import IHP.ModelSupport.Types (ModelContext)+import IHP.RequestVault.Helper+import Wai.Request.Params.Middleware (RequestBody (..), requestBodyVaultKey)++-- request.modelContext+modelContextVaultKey :: Vault.Key ModelContext+modelContextVaultKey = unsafePerformIO Vault.newKey+{-# NOINLINE modelContextVaultKey #-}++{-# INLINE modelContextMiddleware #-}+modelContextMiddleware :: ModelContext -> Middleware+modelContextMiddleware = insertVaultMiddleware modelContextVaultKey++{-# INLINE requestModelContext #-}+requestModelContext :: Request -> ModelContext+requestModelContext = lookupRequestVault modelContextVaultKey++-- request.parsedBody+{-# INLINE requestParsedBody #-}+requestParsedBody :: Request -> RequestBody+requestParsedBody = lookupRequestVault requestBodyVaultKey++-- Field access helpers+instance HasField "modelContext" Request ModelContext where+ {-# INLINE getField #-}+ getField request = requestModelContext request+instance HasField "parsedBody" Request RequestBody where+ {-# INLINE getField #-}+ getField request = requestParsedBody request
@@ -5,8 +5,30 @@ -} module IHP.Router.Types where -import IHP.Prelude+import Prelude+import Data.ByteString (ByteString)+import Control.Exception (Exception) import Network.HTTP.Types.Method+import qualified Data.HashMap.Strict as HashMap+import Data.Attoparsec.ByteString.Char8 (Parser)+import Network.Wai (Application)++-- | A controller route entry — either a pre-built HashMap for O(1) dispatch,+-- or a custom Attoparsec parser for dynamic URL patterns.+--+-- 'ControllerRouteMap' carries both an auto-route HashMap and a fallback parser+-- for custom routes. 'ControllerRouteParser' wraps standalone parsers like 'get', 'post',+-- 'webSocketApp', 'startPage', etc.+--+-- 'frontControllerToWAIApp' scans all 'ControllerRouteMap' HashMaps directly (no Attoparsec)+-- for O(1) dispatch, and only falls back to Attoparsec for custom/dynamic route parsers.+data ControllerRoute application+ = ControllerRouteMap+ !(HashMap.HashMap ByteString (application -> Application))+ (Parser Application)+ -- ^ Auto-route HashMap + custom routes fallback parser (lazy — only evaluated on HashMap miss)+ | ControllerRouteParser !(Parser Application)+ -- ^ Custom route parser (for get, post, webSocketApp, startPage, etc.) data TypedAutoRouteError = BadType
@@ -0,0 +1,29 @@+{-|+Module: IHP.Router.UrlGenerator+Description: Type class for URL path generation+Copyright: (c) digitally induced GmbH, 2020++Provides the 'HasPath' type class used for generating URL paths from controller actions.+This is a lightweight module with minimal dependencies, allowing modules that only need+path generation to avoid importing the full routing infrastructure.+-}+module IHP.Router.UrlGenerator+( HasPath (..)+) where++import Data.Text (Text)++-- | Type class for types that can be converted to URL paths.+--+-- This is used by IHP's routing system to generate URLs for controller actions.+--+-- __Example:__+--+-- >>> pathTo UsersAction+-- "/Users"+--+-- >>> pathTo ShowUserAction { userId = "a32913dd-ef80-4f3e-9a91-7879e17b2ece" }+-- "/ShowUser?userId=a32913dd-ef80-4f3e-9a91-7879e17b2ece"+--+class HasPath controller where+ pathTo :: controller -> Text
@@ -1,3 +1,4 @@+{-# OPTIONS_HADDOCK not-home, hide #-} {-| Module: IHP.RouterPrelude Description: Prelude used by modules like Web.Routes, Admin.Routes, etc.
@@ -1,4 +1,4 @@-{-# LANGUAGE AllowAmbiguousTypes, UndecidableInstances, LambdaCase #-}+{-# LANGUAGE AllowAmbiguousTypes, UndecidableInstances, LambdaCase, ScopedTypeVariables #-} module IHP.RouterSupport ( CanRoute (..) , HasPath (..)@@ -28,16 +28,27 @@ , onlyAllowMethods , getMethod , routeParam+, withImplicits+, applyConstr+, ControllerRoute (..)+, findInRouteMaps+, buildAutoRouteMap ) where -import qualified Prelude-import ClassyPrelude hiding (index, delete, take)+import Prelude hiding (take)+import Data.ByteString (ByteString)+import Data.Text (Text)+import Data.Maybe (fromMaybe, mapMaybe)+import Data.List (find, isPrefixOf)+import Control.Monad (unless, join)+import Control.Applicative ((<|>), empty)+import Text.Read (readMaybe)+import Control.Exception.Safe (SomeException, fromException)+import Control.Exception (evaluate) import qualified IHP.ModelSupport as ModelSupport import IHP.FrameworkConfig-import IHP.ApplicationContext hiding (frameworkConfig) import Data.UUID import Network.HTTP.Types.Method-import IHP.Controller.RequestContext import Network.Wai import IHP.ControllerSupport import Data.Attoparsec.ByteString.Char8 (string, Parser, parseOnly, take, endOfInput, choice, takeTill, takeByteString)@@ -47,82 +58,107 @@ import qualified Control.Monad.State.Strict as State import qualified Data.Text as Text import Network.HTTP.Types.URI+import Network.HTTP.Types.Status (status400)+import Network.HTTP.Types.Header (hContentType) import qualified Data.List as List import Unsafe.Coerce import IHP.HaskellSupport hiding (get) import qualified Data.Typeable as Typeable import qualified Data.ByteString.Char8 as ByteString-import qualified Data.Char as Char-import Control.Monad.Fail import Data.String.Conversions (ConvertibleStrings (convertString), cs) import qualified Text.Blaze.Html5 as Html5 import qualified IHP.ErrorController as ErrorController import qualified Control.Exception as Exception-import qualified Data.List.Split as List import qualified Network.URI.Encode as URI import qualified Data.Text.Encoding as Text import Data.Dynamic import IHP.Router.Types+import IHP.Router.UrlGenerator+import qualified Data.HashMap.Strict as HashMap import IHP.WebSocket (WSApp) import qualified IHP.WebSocket as WS import GHC.TypeLits as T import IHP.Controller.Context import IHP.Controller.Param-import qualified Data.TMap as TMap-import qualified IHP.ApplicationContext as ApplicationContext import Data.Kind+import qualified Data.TMap as TypeMap+import IHP.Controller.Response (ResponseException(..)) +-- | Binds @?request@ and @?respond@ from WAI arguments, then runs the given action.+--+-- This avoids repeating @let ?request = waiRequest; let ?respond = waiRespond@ at each call site.+{-# INLINE withImplicits #-}+withImplicits :: ((?request :: Request, ?respond :: Respond) => Application) -> Application+withImplicits action waiRequest waiRespond =+ let ?request = waiRequest+ ?respond = waiRespond+ in action waiRequest waiRespond+ runAction' :: forall application controller . ( Controller controller- , ?applicationContext :: ApplicationContext , InitControllerContext application , ?application :: application , Typeable application , Typeable controller ) => controller -> Application-runAction' controller request respond = do- let ?modelContext = ApplicationContext.modelContext ?applicationContext- requestContext <- createRequestContext ?applicationContext request respond- let ?context = requestContext- let ?requestContext = requestContext- contextOrErrorResponse <- newContextForAction controller- case contextOrErrorResponse of- Left res -> res- Right context -> let ?context = context in runAction controller-{-# INLINABLE runAction' #-}+runAction' controller waiRequest waiRespond = do+ (context, maybeException) <- setupActionContext @application (Typeable.typeOf controller) waiRequest waiRespond+ let ?context = context+ let ?respond = waiRespond+ let ?request = context.request+ case maybeException of+ Just exception ->+ case fromException exception of+ Just (ResponseException response) -> waiRespond response+ Nothing -> ErrorController.displayException exception controller " while calling initContext"+ Nothing -> do+ let ?modelContext = ?request.modelContext+ runAction controller+{-# INLINE runAction' #-} class FrontController application where controllers- :: (?applicationContext :: ApplicationContext, ?application :: application, ?context :: RequestContext)- => [Parser Application]+ :: (?application :: application, ?request :: Request, ?respond :: Respond)+ => [ControllerRoute application] router- :: (?applicationContext :: ApplicationContext, ?application :: application, ?context :: RequestContext)- => [Parser Application] -> Parser Application+ :: (?application :: application, ?request :: Request, ?respond :: Respond)+ => [ControllerRoute application] -> Parser Application router = defaultRouter {-# INLINABLE router #-} defaultRouter- :: (?applicationContext :: ApplicationContext, ?application :: application, ?context :: RequestContext, FrontController application)- => [Parser Application] -> Parser Application-defaultRouter additionalControllers = do- let allControllers = controllers <> additionalControllers- applications <- choice $ map (\r -> r <* endOfInput) allControllers- pure applications+ :: (?application :: application, ?request :: Request, ?respond :: Respond, FrontController application)+ => [ControllerRoute application] -> Parser Application+defaultRouter additionalRoutes = do+ let allRoutes = controllers <> additionalRoutes+ path = rawPathInfo ?request+ -- Fast path: check auto-route HashMaps directly (no Attoparsec string matching)+ case findInRouteMaps path allRoutes of+ Just handler -> takeByteString *> pure (handler ?application)+ Nothing -> do+ -- Slow path: fall back to Attoparsec parsers for custom/dynamic routes+ let parsers = concatMap getRouteParsers allRoutes+ choice (map (<* endOfInput) parsers) {-# INLINABLE defaultRouter #-} -class HasPath controller where- -- | Returns the path to a given action- --- -- >>> pathTo UsersAction- -- "/Users"- --- -- >>> pathTo ShowUserAction { userId = "a32913dd-ef80-4f3e-9a91-7879e17b2ece" }- -- "/ShowUser?userId=a32913dd-ef80-4f3e-9a91-7879e17b2ece"- pathTo :: controller -> Text+-- | Scan 'ControllerRouteMap' entries for a matching path.+-- Returns as soon as a HashMap contains the path. Skips 'ControllerRouteParser' entries.+findInRouteMaps :: ByteString -> [ControllerRoute application] -> Maybe (application -> Application)+findInRouteMaps _ [] = Nothing+findInRouteMaps path (ControllerRouteMap m _ : rest) =+ case HashMap.lookup path m of+ Just handler -> Just handler+ Nothing -> findInRouteMaps path rest+findInRouteMaps path (_ : rest) = findInRouteMaps path rest +-- | Extract fallback parsers from controller routes.+getRouteParsers :: ControllerRoute application -> [Parser Application]+getRouteParsers (ControllerRouteMap _ fallback) = [fallback]+getRouteParsers (ControllerRouteParser p) = [p]+ -- | Returns the url to a given action. -- -- Uses the baseUrl configured in @Config/Config.hs@. When no @baseUrl@@@ -139,9 +175,37 @@ {-# INLINE urlTo #-} class HasPath controller => CanRoute controller where- parseRoute' :: (?context :: RequestContext) => Parser controller+ parseRoute' :: (?request :: Request, ?respond :: Respond) => Parser controller + -- | Builds a WAI Application parser for this controller.+ --+ -- The default implementation parses the controller action using 'parseRoute''+ -- and applies the given callback. The overlappable 'AutoRoute' instance overrides+ -- this to defer query string parsing and method validation to the Application closure.+ parseRouteWithAction :: (?request :: Request, ?respond :: Respond) => (controller -> Application) -> Parser Application+ parseRouteWithAction toApp = do+ action <- parseRoute'+ pure (toApp action)+ {-# INLINE parseRouteWithAction #-} + -- | Build a 'ControllerRoute' for this controller.+ --+ -- The default wraps the parser in 'ControllerRouteParser'.+ -- The overlappable 'AutoRoute' instance overrides this to use 'ControllerRouteMap'+ -- for O(1) HashMap dispatch. This is what 'parseRoute' calls.+ toControllerRoute :: forall application.+ ( ?request :: Request+ , ?respond :: Respond+ , Controller controller+ , InitControllerContext application+ , ?application :: application+ , Typeable application+ , Typeable controller+ ) => ControllerRoute application+ toControllerRoute = ControllerRouteParser (parseRouteWithAction @controller (runAction' @application))+ {-# INLINABLE toControllerRoute #-}++ -- | Each of these is tried when trying to parse an argument to a controller constructor (i.e. in IHP, an action). -- The type @d@ is an the type of the argument, and all we know about this type that its conforms to @Data@. -- We cannot cast @d@ to some arbitrary type, since adding additional constraints to @d@ (such as Read)@@ -157,12 +221,13 @@ -- Try and parse @Int@ or @Maybe Int@ \case Just queryValue -> case eqT :: Maybe (d :~: Int) of- Just Refl -> readMay (cs queryValue :: String)- |> \case- Just int -> Right int- Nothing -> Left BadType { field = "", value = Just queryValue, expectedType = "Int" }+ Just Refl -> case ByteString.readInt queryValue of+ Just (n, "") -> Right n+ _ -> Left BadType { field = "", value = Just queryValue, expectedType = "Int" } Nothing -> case eqT :: Maybe (d :~: Maybe Int) of- Just Refl -> Right $ readMay (cs queryValue :: String)+ Just Refl -> Right $ case ByteString.readInt queryValue of+ Just (n, "") -> Just n+ _ -> Nothing Nothing -> Left NotMatched Nothing -> case eqT :: Maybe (d :~: Maybe Int) of Just Refl -> Right Nothing@@ -170,12 +235,13 @@ \case Just queryValue -> case eqT :: Maybe (d :~: Integer) of- Just Refl -> readMay (cs queryValue :: String)- |> \case- Just int -> Right int- Nothing -> Left BadType { field = "", value = Just queryValue, expectedType = "Integer" }+ Just Refl -> case ByteString.readInteger queryValue of+ Just (n, "") -> Right n+ _ -> Left BadType { field = "", value = Just queryValue, expectedType = "Integer" } Nothing -> case eqT :: Maybe (d :~: Maybe Integer) of- Just Refl -> Right $ readMay (cs queryValue :: String)+ Just Refl -> Right $ case ByteString.readInteger queryValue of+ Just (n, "") -> Just n+ _ -> Nothing Nothing -> Left NotMatched Nothing -> case eqT :: Maybe (d :~: Maybe Integer) of Just Refl -> Right Nothing@@ -209,18 +275,16 @@ -- Try and parse @[Int]@. If value is not present then default to empty list. \queryValue -> case eqT :: Maybe (d :~: [Int]) of Just Refl -> case queryValue of- Just queryValue -> Text.splitOn "," (cs queryValue)- |> map readMay- |> catMaybes+ Just queryValue -> ByteString.split ',' queryValue+ |> mapMaybe (\b -> case ByteString.readInt b of Just (n, "") -> Just n; _ -> Nothing) |> Right Nothing -> Right [] Nothing -> Left NotMatched, \queryValue -> case eqT :: Maybe (d :~: [Integer]) of Just Refl -> case queryValue of- Just queryValue -> Text.splitOn "," (cs queryValue)- |> map readMay- |> catMaybes+ Just queryValue -> ByteString.split ',' queryValue+ |> mapMaybe (\b -> case ByteString.readInteger b of Just (n, "") -> Just n; _ -> Nothing) |> Right Nothing -> Right [] Nothing -> Left NotMatched,@@ -228,11 +292,8 @@ -- Try and parse a raw [UUID] \queryValue -> case eqT :: Maybe (d :~: [UUID]) of Just Refl -> case queryValue of- Just queryValue -> queryValue- |> cs- |> Text.splitOn ","- |> map (fromASCIIBytes . cs)- |> catMaybes+ Just queryValue -> ByteString.split ',' queryValue+ |> mapMaybe fromASCIIBytes |> Right Nothing -> Right [] Nothing -> Left NotMatched,@@ -265,7 +326,7 @@ Nothing -> Left BadType { field = "", value = Just queryValue, expectedType = "UUID" } Nothing -> Left NotMatched ]-{-# INLINABLE parseFuncs #-}+{-# NOINLINE parseFuncs #-} -- | As we fold over a constructor, we want the values parsed from the query string -- to be in the same order as they are in the constructor.@@ -281,7 +342,7 @@ querySortedByFields query constructor = constrFields constructor |> map cs |> map (\field -> (field, join $ List.lookup field query))-{-# INLINABLE querySortedByFields #-}+{-# NOINLINE querySortedByFields #-} -- | Given a constructor and a parsed query string, attempt to construct a value of the constructor's type. -- For example, given the controller@@ -330,53 +391,47 @@ Right (x, []) -> pure x Right (_) -> Left TooFewArguments Left e -> Left e -- runtime type error-{-# INLINABLE applyConstr #-}+{-# NOINLINE applyConstr #-} class Data controller => AutoRoute controller where- autoRouteWithIdType :: (?context :: RequestContext, Data idType) => (ByteString -> Maybe idType) -> Parser controller+ autoRouteWithIdType :: (?request :: Request, ?respond :: Respond, Data idType) => (ByteString -> Maybe idType) -> Parser controller autoRouteWithIdType parseIdFunc = let- allConstructors :: [Constr]- allConstructors = dataTypeConstrs (dataTypeOf (Prelude.undefined :: controller))- query :: Query- query = queryString ?context.request-- paramValues :: [ByteString]- paramValues = catMaybes $ map snd query-- parseAction :: Constr -> Parser controller- parseAction constr = let- prefix :: ByteString- prefix = ByteString.pack (actionPrefix @controller)-- actionName = ByteString.pack (showConstr constr)-- actionPath :: ByteString- actionPath = stripActionSuffixByteString actionName-- allowedMethods = allowedMethodsForAction @controller actionName-- checkRequestMethod action = do- method <- getMethod- unless (allowedMethods |> includes method) (Exception.throw UnexpectedMethodException { allowedMethods, method })- pure action-- action = case applyConstr parseIdFunc constr query of- Right parsedAction -> pure parsedAction- Left e -> Exception.throw e-- in do- parsedAction <- string prefix >> (string actionPath <* endOfInput) *> action- checkRequestMethod parsedAction-- in choice (map parseAction allConstructors)+ query = queryString ?request+ in do+ -- routeMatchParser is a CAF (no ?request dependency), computed once per controller type.+ -- It handles the static string matching against URL paths.+ (constr, allowedMethods) <- routeMatchParser @controller+ action <- case applyConstr parseIdFunc constr query of+ Right parsedAction -> pure parsedAction+ Left e -> Exception.throw e+ method <- getMethod+ unless (allowedMethods |> includes method) (Exception.throw UnexpectedMethodException { allowedMethods, method })+ pure action {-# INLINABLE autoRouteWithIdType #-} - autoRoute :: (?context :: RequestContext) => Parser controller+ autoRoute :: (?request :: Request, ?respond :: Respond) => Parser controller autoRoute = autoRouteWithIdType (\_ -> Nothing :: Maybe Integer) {-# INLINABLE autoRoute #-} + -- | Constructs a controller value from a matched constructor and query string.+ --+ -- Uses the same id parser as 'autoRoute'. Override this when you override+ -- 'autoRoute' with 'autoRouteWithIdType' to keep them in sync.+ --+ -- This is used by 'parseRoute' to defer query string parsing to the Application+ -- closure while keeping path matching in the parser.+ --+ -- __Example:__+ --+ -- > instance AutoRoute MyController where+ -- > autoRoute = autoRouteWithIdType (parseIntegerId @(Id MyModel))+ -- > applyAction = applyConstr (parseIntegerId @(Id MyModel))+ applyAction :: Constr -> Query -> Either TypedAutoRouteError controller+ applyAction = applyConstr (\_ -> Nothing :: Maybe Integer)+ {-# INLINE applyAction #-}+ -- | Specifies the allowed HTTP methods for a given action -- -- The default implementation does a smart guess based on the@@ -409,56 +464,122 @@ _ -> [GET, POST, HEAD] {-# INLINE allowedMethodsForAction #-} + -- | Custom route parser for overriding individual action routes.+ --+ -- Use this to provide custom URL patterns for specific actions while keeping+ -- the auto-generated routes for all other actions.+ --+ -- The custom routes are tried first, before the auto-generated routes.+ -- The auto-generated route for the overridden action still works as a fallback.+ --+ -- __Example:__+ --+ -- > instance AutoRoute PostsController where+ -- > customRoutes = do+ -- > string "/posts/"+ -- > postId <- parseId+ -- > endOfInput+ -- > onlyAllowMethods [GET, HEAD]+ -- > pure ShowPostAction { postId }+ --+ customRoutes :: (?request :: Request, ?respond :: Respond) => Parser controller+ customRoutes = empty+ {-# INLINE customRoutes #-}++ -- | Custom path generation for overriding individual action URLs.+ --+ -- Use this together with 'customRoutes' to generate custom URLs for specific+ -- actions while keeping the auto-generated URLs for all other actions.+ --+ -- Return @Just path@ for actions with custom URLs, or @Nothing@ to fall back+ -- to the auto-generated URL.+ --+ -- __Example:__+ --+ -- > instance AutoRoute PostsController where+ -- > customPathTo ShowPostAction { postId } = Just ("/posts/" <> tshow postId)+ -- > customPathTo _ = Nothing+ --+ customPathTo :: controller -> Maybe Text+ customPathTo _ = Nothing+ {-# INLINE customPathTo #-}++-- | Static route-matching parser that becomes a CAF when specialized for a concrete+-- controller type. Uses a 'HashMap' for O(1) action name lookup after matching the prefix.+-- Since it doesn't reference @?request@ or @?respond@, GHC can float this out as a+-- top-level constant.+routeMatchParser :: forall controller. (Data controller, AutoRoute controller) => Parser (Constr, [StdMethod])+routeMatchParser = do+ string prefix+ remaining <- takeByteString+ case HashMap.lookup remaining actionMatchMap of+ Just result -> pure result+ Nothing -> fail "no matching action"+ where+ prefix :: ByteString+ prefix = Text.encodeUtf8 (actionPrefixText @controller)++ actionMatchMap :: HashMap.HashMap ByteString (Constr, [StdMethod])+ actionMatchMap = HashMap.fromList+ [ (actionPath, (constr, allowedMethods))+ | constr <- dataTypeConstrs (dataTypeOf (Prelude.undefined :: controller))+ , let actionName = ByteString.pack (showConstr constr)+ actionPath = stripActionSuffixByteString actionName+ allowedMethods = allowedMethodsForAction @controller actionName+ ]+{-# NOINLINE routeMatchParser #-}+ -- | Returns the url prefix for a controller. The prefix is based on the -- module where the controller is defined. -- -- All controllers defined in the `Web/` directory don't have a prefix at all. -- -- E.g. controllers in the `Admin/` directory are prefixed with @/admin/@.-actionPrefix :: forall (controller :: Type). Typeable controller => String-actionPrefix =- case moduleName of- ('W':'e':'b':'.':_) -> "/"- ('I':'H':'P':'.':_) -> "/"- ("") -> "/"- moduleName -> "/" <> let prefix = getPrefix "" moduleName in map Char.toLower prefix <> "/"+actionPrefixText :: forall (controller :: Type). Typeable controller => Text+actionPrefixText+ | "Web." `Text.isPrefixOf` moduleName = "/"+ | "IHP." `Text.isPrefixOf` moduleName = "/"+ | Text.null moduleName = "/"+ | otherwise = "/" <> Text.toLower (getPrefix moduleName) <> "/" where- moduleName :: String- moduleName = Typeable.typeOf (error "unreachable" :: controller)+ moduleName :: Text+ moduleName = Text.pack $ Typeable.typeOf (error "unreachable" :: controller) |> Typeable.typeRepTyCon |> Typeable.tyConModule - -- E.g. getPrefix "" "Admin.User" == "Admin"- getPrefix prefix ('.':_) = prefix- getPrefix prefix (x:xs) = getPrefix (prefix <> [x]) xs- getPrefix prefix [] = prefix--{-# INLINE actionPrefix #-}+ getPrefix :: Text -> Text+ getPrefix t = fst (Text.breakOn "." t)+{-# NOINLINE actionPrefixText #-} --- | Strips the "Action" at the end of action names+-- | Strips the "Action" suffix from action names ----- >>> stripActionSuffixString "ShowUserAction"+-- >>> stripActionSuffixByteString "ShowUserAction" -- "ShowUser" ----- >>> stripActionSuffixString "UsersAction"+-- >>> stripActionSuffixByteString "UsersAction" -- "UsersAction" ----- >>> stripActionSuffixString "User"+-- >>> stripActionSuffixByteString "User" -- "User"-stripActionSuffixString :: String -> String-stripActionSuffixString string =- case string of- "Action" -> ""- (x:xs) -> x : stripActionSuffixString xs- "" -> ""-{-# INLINE stripActionSuffixString #-}---- | Like 'stripActionSuffixString' but for ByteStrings stripActionSuffixByteString :: ByteString -> ByteString stripActionSuffixByteString actionName = fromMaybe actionName (ByteString.stripSuffix "Action" actionName) {-# INLINE stripActionSuffixByteString #-} +-- | Strips the "Action" suffix from action names+--+-- >>> stripActionSuffixText "ShowUserAction"+-- "ShowUser"+--+-- >>> stripActionSuffixText "UsersAction"+-- "UsersAction"+--+-- >>> stripActionSuffixText "User"+-- "User"+stripActionSuffixText :: Text -> Text+stripActionSuffixText actionName = fromMaybe actionName (Text.stripSuffix "Action" actionName)+{-# INLINE stripActionSuffixText #-} + -- | Returns the create action for a given controller. -- Example: `createAction @UsersController == Just CreateUserAction` createAction :: forall controller. AutoRoute controller => Maybe controller@@ -471,7 +592,7 @@ allConstructors = dataTypeConstrs (dataTypeOf (Prelude.undefined :: controller)) isCreateConstructor :: Constr -> Bool- isCreateConstructor constructor = "Create" `isPrefixOf` showConstr constructor && ClassyPrelude.null (constrFields constructor)+ isCreateConstructor constructor = "Create" `isPrefixOf` showConstr constructor && Prelude.null (constrFields constructor) {-# INLINE createAction #-} -- | Returns the update action when given a controller and id.@@ -501,9 +622,41 @@ {-# INLINE updateAction #-} instance {-# OVERLAPPABLE #-} (AutoRoute controller, Controller controller) => CanRoute controller where- parseRoute' = autoRoute+ parseRoute' = customRoutes <|> autoRoute {-# INLINABLE parseRoute' #-} + -- | This is only used as a fallback parser (via lazy thunk in 'ControllerRouteMap').+ -- The primary routing path goes through 'findInRouteMaps' / 'buildAutoRouteMap' instead.+ -- Performance here doesn't matter — keep it simple.+ parseRouteWithAction toApp = (do+ action <- customRoutes @controller+ pure (toApp action)+ ) <|> (do+ (constr, allowedMethods) <- routeMatchParser @controller+ pure $ \waiRequest waiRespond -> do+ case applyAction @controller constr (queryString waiRequest) of+ Left e -> waiRespond $ responseLBS status400 [(hContentType, "text/plain")] (cs $ show e)+ Right action -> do+ case parseMethod (requestMethod waiRequest) of+ Right method -> do+ unless (allowedMethods |> includes method)+ (Exception.throw UnexpectedMethodException { allowedMethods, method })+ toApp action waiRequest waiRespond+ Left err -> error ("Invalid HTTP method: " <> ByteString.unpack err)+ )+ {-# INLINABLE parseRouteWithAction #-}++ -- | Override to use 'ControllerRouteMap' for O(1) HashMap dispatch.+ toControllerRoute :: forall application.+ ( ?request :: Request, ?respond :: Respond, Controller controller+ , InitControllerContext application, ?application :: application+ , Typeable application, Typeable controller+ ) => ControllerRoute application+ toControllerRoute = ControllerRouteMap+ (buildAutoRouteMap @controller @application)+ (parseRouteWithAction @controller (runAction' @application))+ {-# INLINABLE toControllerRoute #-}+ -- | Instances of the @QueryParam@ type class can be represented in URLs as query parameters. -- Currently this is only Int, Text, and both wrapped in List and Maybe. -- IDs also are representable in a URL, but we are unable to match on polymorphic types using reflection,@@ -532,96 +685,69 @@ instance {-# OVERLAPPABLE #-} (Show controller, AutoRoute controller) => HasPath controller where {-# INLINABLE pathTo #-}- pathTo !action = Text.pack (appPrefix <> actionName <> arguments)+ pathTo !action = case customPathTo action of+ Just path -> path+ Nothing ->+ let !ci = constrIndex (toConstr action) - 1+ (!basePath, !fieldNames) = constrInfoCache !! ci+ in case fieldNames of+ [] -> basePath+ _ ->+ let !fieldValues = gmapQ renderFieldForUrl action+ in basePath <> buildQueryText fieldNames fieldValues where- appPrefix :: String- !appPrefix = actionPrefix @controller-- actionName :: String- !actionName = stripActionSuffixString $! showConstr constructor-- constructor = toConstr action-- stripQuotes ('"':rest) = List.init rest- stripQuotes otherwise = otherwise-- -- | The @gmapQ@ function allows us to iterate over each term in a constructor function and- -- build a list of results from performing some function on each term.- -- Here we send each term through @constrShow@, giving us our preferred representation for- -- use in URLs.- showTerms :: controller -> [Maybe String]- showTerms = gmapQ (constrShow typeShows)-- -- | @constrShow@ tries to convert each value @d@ into a String representation.- -- If one passes, return it immediately, otherwise try all the defined @typeShow@ functions.- constrShow :: Data d => [(d -> Maybe String)] -> d -> Maybe String- constrShow [] _ = Nothing- constrShow (f:fs) d = case f d of- Just str -> Just str- Nothing -> constrShow fs d+ -- Precomputed per constructor: (basePath, fieldNames).+ -- Does not reference 'action', so GHC floats this out as a CAF+ -- when the instance is specialized for a concrete controller type.+ constrInfoCache :: [(Text, [Text])]+ constrInfoCache = map mkInfo allConstrs+ where+ allConstrs = dataTypeConstrs (dataTypeOf (Prelude.undefined :: controller))+ !appPrefix = actionPrefixText @controller+ mkInfo c =+ let !bp = appPrefix <> stripActionSuffixText (Text.pack (showConstr c))+ !fns = map Text.pack (constrFields c)+ in (bp, fns) - -- | Try and match some value to all of the types we can represent in a URL.- -- Only type not contained in here is the "Id" type, since we cannot match- -- on polymorphic types.- typeShows :: forall d. Data d => [(d -> Maybe String)]- typeShows = [- \val -> (eqT :: Maybe (d :~: Text))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: [Text]))- >>= \Refl -> Just (showQueryParam (val :: [Text])),- \val -> (eqT :: Maybe (d :~: Maybe Text))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: Int))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: [Int]))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: Maybe Int))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: Integer))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: [Integer]))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: Maybe Integer))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: UUID))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: Maybe UUID))- >>= \Refl -> Just (showQueryParam val),- \val -> (eqT :: Maybe (d :~: [UUID]))- >>= \Refl -> Just (showQueryParam val)- ]+ buildQueryText :: [Text] -> [Text] -> Text+ buildQueryText names values =+ zip names values+ |> filter (\(_, v) -> not (Text.null v))+ |> map (\(k, v) -> k <> "=" <> URI.encodeText v)+ |> Text.intercalate "&"+ |> (\q -> if Text.null q then q else Text.cons '?' q) - arguments :: String- !arguments = show action -- `SomeRecord { a = b, c = d }`- |> List.filter (/= ' ')- |> List.break (== '{')- |> snd- |> List.drop 1- |> List.break (== '}')- |> fst -- `a=b,c=d`- |> List.splitWhen (== ',') -- ["a=b", "c=d"]- |> map (\s -> let (key, value) = List.break (== '=') s in (key, List.drop 1 value))- |> map (\(k ,v) -> (k, stripQuotes v)) -- "value" -> value- |> filter (\(k, v) -> (not . List.null) k && (not . List.null) v)- -- At this point we have a list of keys and values as represented by @show@.- -- For Lists and Maybe types, we want to represent these in a different way,- -- so we construct another list of values using type reflection and the QueryParam type class.- |> \(kvs :: [(String, String)]) -> zip (showTerms action) kvs- -- If an Id type was present in the action, it will be returned as Nothing by @showTerms@- -- as we are not able to match on the type using reflection.- -- In this case we default back to the @show@ representation.- |> map (\(v1, (k, v2)) -> (k, fromMaybe v2 v1))- |> map (\(k, v) -> if isEmpty v- then ""- else k <> "=" <> URI.encode v)- |> List.filter (not . isEmpty)- |> List.intercalate "&"- |> (\q -> if List.null q then q else '?':q)+-- | Render a controller field value as 'Text' for URL query parameter inclusion.+--+-- Uses type reflection ('eqT') to match known types (Text, UUID, Int, Integer,+-- and their Maybe\/List variants). For newtypes like @Id'@ that wrap a known type,+-- falls back to recursive unwrap via 'gmapQ'.+renderFieldForUrl :: forall d. Data d => d -> Text+renderFieldForUrl val+ -- UUID first: most common type after Id newtype unwrap+ | Just Refl <- eqT @d @UUID = toText val+ | Just Refl <- eqT @d @Text = val+ | Just Refl <- eqT @d @Int = Text.pack (show val)+ | Just Refl <- eqT @d @Integer = Text.pack (show val)+ | Just Refl <- eqT @d @(Maybe UUID) = maybe "" toText val+ | Just Refl <- eqT @d @(Maybe Text) = maybe "" id val+ | Just Refl <- eqT @d @(Maybe Int) = maybe "" (Text.pack . show) val+ | Just Refl <- eqT @d @(Maybe Integer) = maybe "" (Text.pack . show) val+ | Just Refl <- eqT @d @[UUID] = Text.intercalate "," (map toText val)+ | Just Refl <- eqT @d @[Text] = Text.intercalate "," (val :: [Text])+ | Just Refl <- eqT @d @[Int] = Text.intercalate "," (map (Text.pack . show) val)+ | Just Refl <- eqT @d @[Integer] = Text.intercalate "," (map (Text.pack . show) val)+ | otherwise =+ -- Unwrap one layer for newtypes (e.g., Id' wrapping UUID, Maybe (Id' table))+ case gmapQ renderFieldForUrl val of+ [inner] -> inner+ _ -> ""+{-# INLINABLE renderFieldForUrl #-} -- | Parses the HTTP Method from the request and returns it.-getMethod :: (?context :: RequestContext) => Parser StdMethod+getMethod :: (?request :: Request, ?respond :: Respond) => Parser StdMethod getMethod =- case parseMethod ?context.request.requestMethod of+ case parseMethod ?request.requestMethod of Left error -> fail (ByteString.unpack error) Right method -> pure method {-# INLINABLE getMethod #-}@@ -641,18 +767,17 @@ get :: (Controller action , InitControllerContext application , ?application :: application- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext , Typeable application , Typeable action- ) => ByteString -> action -> Parser Application-get path action = do- method <- getMethod- case method of- GET -> do- string path- pure (runAction' action)- _ -> fail "Invalid method, expected GET"+ ) => ByteString -> action -> ControllerRoute application+get path action = ControllerRouteParser $ do+ string path+ pure $ \waiRequest waiRespond ->+ case parseMethod (requestMethod waiRequest) of+ Right GET -> runAction' action waiRequest waiRespond+ Right HEAD -> runAction' action waiRequest waiRespond+ Right method -> Exception.throw UnexpectedMethodException { allowedMethods = [GET, HEAD], method }+ Left err -> error ("Invalid HTTP method: " <> ByteString.unpack err) {-# INLINABLE get #-} -- | Routes a given path to an action when requested via POST.@@ -670,18 +795,16 @@ post :: (Controller action , InitControllerContext application , ?application :: application- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext , Typeable application , Typeable action- ) => ByteString -> action -> Parser Application-post path action = do- method <- getMethod- case method of- POST -> do- string path- pure (runAction' action)- _ -> fail "Invalid method, expected POST"+ ) => ByteString -> action -> ControllerRoute application+post path action = ControllerRouteParser $ do+ string path+ pure $ \waiRequest waiRespond ->+ case parseMethod (requestMethod waiRequest) of+ Right POST -> runAction' action waiRequest waiRespond+ Right method -> Exception.throw UnexpectedMethodException { allowedMethods = [POST], method }+ Left err -> error ("Invalid HTTP method: " <> ByteString.unpack err) {-# INLINABLE post #-} -- | Filter methods when writing a custom routing parser@@ -709,7 +832,7 @@ -- > -- > createRecordAction <|> updateRecordAction ---onlyAllowMethods :: (?context :: RequestContext) => [StdMethod] -> Parser ()+onlyAllowMethods :: (?request :: Request, ?respond :: Respond) => [StdMethod] -> Parser () onlyAllowMethods methods = do method <- getMethod unless (method `elem` methods) (fail ("Invalid method, expected one of: " <> show methods))@@ -730,11 +853,9 @@ ( WSApp webSocketApp , InitControllerContext application , ?application :: application- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext , Typeable application , Typeable webSocketApp- ) => Parser Application+ ) => ControllerRoute application webSocketApp = webSocketAppWithCustomPath @webSocketApp typeName where typeName :: ByteString@@ -747,12 +868,10 @@ ( WSApp webSocketApp , InitControllerContext application , ?application :: application- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext , Typeable application , Typeable webSocketApp , Controller webSocketApp- ) => Parser Application+ ) => ControllerRoute application webSocketAppWithHTTPFallback = webSocketAppWithCustomPathAndHTTPFallback @webSocketApp @application typeName where typeName :: ByteString@@ -776,80 +895,145 @@ ( WSApp webSocketApp , InitControllerContext application , ?application :: application- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext , Typeable application , Typeable webSocketApp- ) => ByteString -> Parser Application-webSocketAppWithCustomPath path = do+ ) => ByteString -> ControllerRoute application+webSocketAppWithCustomPath path = ControllerRouteParser $ do Attoparsec.char '/' string path- pure (startWebSocketAppAndFailOnHTTP (WS.initialState @webSocketApp))+ pure $ withImplicits (startWebSocketAppAndFailOnHTTP @webSocketApp @application (WS.initialState @webSocketApp)) {-# INLINABLE webSocketAppWithCustomPath #-} webSocketAppWithCustomPathAndHTTPFallback :: forall webSocketApp application. ( WSApp webSocketApp , InitControllerContext application , ?application :: application- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext , Typeable application , Typeable webSocketApp , Controller webSocketApp- ) => ByteString -> Parser Application-webSocketAppWithCustomPathAndHTTPFallback path = do+ ) => ByteString -> ControllerRoute application+webSocketAppWithCustomPathAndHTTPFallback path = ControllerRouteParser $ do Attoparsec.char '/' string path let action = WS.initialState @webSocketApp- pure (startWebSocketApp action (runActionWithNewContext action))+ pure $ withImplicits (startWebSocketApp @webSocketApp @application action (runActionWithNewContext action)) {-# INLINABLE webSocketAppWithCustomPathAndHTTPFallback #-} -- | Defines the start page for a router (when @\/@ is requested).-startPage :: forall action application. (Controller action, InitControllerContext application, ?application::application, ?applicationContext::ApplicationContext, ?context::RequestContext, Typeable application, Typeable action) => action -> Parser Application-startPage action = get (ByteString.pack (actionPrefix @action)) action+startPage :: forall action application. (Controller action, InitControllerContext application, ?application::application, Typeable application, Typeable action) => action -> ControllerRoute application+startPage action = get (Text.encodeUtf8 (actionPrefixText @action)) action {-# INLINABLE startPage #-} withPrefix prefix routes = string prefix >> choice (map (\r -> r <* endOfInput) routes) {-# INLINABLE withPrefix #-} -frontControllerToWAIApp :: forall app (autoRefreshApp :: Type). (?applicationContext :: ApplicationContext, FrontController app, WSApp autoRefreshApp, Typeable autoRefreshApp, InitControllerContext ()) => Middleware -> app -> Application -> Application-frontControllerToWAIApp middleware application notFoundAction request respond = do- let requestContext = RequestContext { request, respond, requestBody = FormBody { params = [], files = [] }, frameworkConfig = ?applicationContext.frameworkConfig }+frontControllerToWAIApp :: forall app (autoRefreshApp :: Type). (FrontController app, WSApp autoRefreshApp, Typeable autoRefreshApp, InitControllerContext ()) => Middleware -> app -> Application -> Application+frontControllerToWAIApp middleware application notFoundAction waiRequest waiRespond = do+ let+ -- Use lazy pattern to defer vault lookup until environment is actually needed+ -- This is needed for tests that don't have frameworkConfig in the vault+ ~environment = waiRequest.frameworkConfig.environment - let ?context = requestContext+ let ?request = waiRequest+ let ?respond = waiRespond - let- path = request.rawPathInfo- handleException :: SomeException -> IO (Either String Application)- handleException exception = pure $ Right $ ErrorController.handleRouterException exception+ let autoRefreshWSParser :: Parser Application+ autoRefreshWSParser =+ let ?application = () in+ let typeName = Typeable.typeOf (error "unreachable" :: autoRefreshApp)+ |> show |> ByteString.pack+ in do+ Attoparsec.char '/'+ string typeName+ pure $ withImplicits (startWebSocketAppAndFailOnHTTP @autoRefreshApp @() (WS.initialState @autoRefreshApp)) - routes = let ?application = application in router [let ?application = () in webSocketApp @autoRefreshApp]+ let allRoutes = let ?application = application in+ ControllerRouteParser autoRefreshWSParser : controllers @app - routedAction :: Either String Application <-- (do- res <- evaluate $ parseOnly (routes <* endOfInput) path- case res of- Left s -> pure $ Left s- Right action -> do- pure $ Right action- )- `Exception.catch` handleException- case routedAction of- Left message -> notFoundAction request respond- Right action -> (middleware action) request respond+ let path = waiRequest.rawPathInfo++ -- Fast path: scan auto-route HashMaps directly (no Attoparsec overhead)+ case findInRouteMaps path allRoutes of+ Just handler -> (middleware (handler application)) waiRequest waiRespond+ Nothing -> do+ -- Slow path: Attoparsec for custom/dynamic route parsers only+ let handleException :: SomeException -> IO (Either String Application)+ handleException exception = pure $ Right $ ErrorController.handleRouterException environment exception++ customParsers = concatMap getRouteParsers allRoutes++ routedAction :: Either String Application <-+ (do+ res <- evaluate $ parseOnly (choice (map (<* endOfInput) customParsers)) path+ case res of+ Left s -> pure $ Left s+ Right action -> pure $ Right action+ )+ `Exception.catch` handleException+ case routedAction of+ Left _ -> notFoundAction waiRequest waiRespond+ Right action -> (middleware action) waiRequest waiRespond {-# INLINABLE frontControllerToWAIApp #-} -mountFrontController :: forall frontController. (?applicationContext :: ApplicationContext, ?context :: RequestContext, FrontController frontController) => frontController -> Parser Application-mountFrontController application = let ?application = application in router []+mountFrontController :: forall frontController application. (?request :: Request, ?respond :: Respond, FrontController frontController) => frontController -> ControllerRoute application+mountFrontController application = ControllerRouteParser (let ?application = application in router []) {-# INLINABLE mountFrontController #-} -parseRoute :: forall controller application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, Controller controller, CanRoute controller, InitControllerContext application, ?application :: application, Typeable application, Typeable controller) => Parser Application-parseRoute = do- action <- parseRoute' @controller- pure $ runAction' @application action+-- | Create a route entry for a controller.+--+-- Automatically uses the HashMap fast path when 'AutoRoute' is available+-- (via the overlappable 'CanRoute' instance), or falls back to Attoparsec+-- for controllers with custom 'CanRoute' instances.+--+-- No user code changes needed — @parseRoute \@PostsController@ picks the+-- optimal strategy at compile time.+parseRoute :: forall controller application.+ ( ?request :: Request+ , ?respond :: Respond+ , CanRoute controller+ , Controller controller+ , InitControllerContext application+ , ?application :: application+ , Typeable application+ , Typeable controller+ ) => ControllerRoute application+parseRoute = toControllerRoute @controller @application {-# INLINABLE parseRoute #-} +-- | Build a HashMap from full paths (prefix + action name) to Application closures.+-- The Application closures take the application value explicitly and handle query string+-- parsing, method validation, and controller execution.+-- Computed once per (controller, application) type pair (NOINLINE CAF).+buildAutoRouteMap :: forall controller application.+ ( AutoRoute controller+ , Controller controller+ , InitControllerContext application+ , Typeable application+ , Typeable controller+ ) => HashMap.HashMap ByteString (application -> Application)+buildAutoRouteMap = HashMap.fromList+ [ (prefix <> actionPath, handler)+ | constr <- dataTypeConstrs (dataTypeOf (Prelude.undefined :: controller))+ , let actionName = ByteString.pack (showConstr constr)+ actionPath = stripActionSuffixByteString actionName+ allowedMethods = allowedMethodsForAction @controller actionName+ handler app waiRequest waiRespond =+ let ?application = app+ in case parseMethod (requestMethod waiRequest) of+ Left err -> error ("Invalid HTTP method: " <> ByteString.unpack err)+ Right method -> do+ unless (allowedMethods |> includes method)+ (Exception.throw UnexpectedMethodException { allowedMethods, method })+ case applyAction @controller constr (queryString waiRequest) of+ Left e -> waiRespond $ responseLBS status400 [(hContentType, "text/plain")] (cs $ show e)+ Right action -> runAction' @application action waiRequest waiRespond+ ]+ where+ prefix :: ByteString+ prefix = Text.encodeUtf8 (actionPrefixText @controller)+{-# NOINLINE buildAutoRouteMap #-}+ parseUUIDOrTextId :: ByteString -> Maybe Dynamic parseUUIDOrTextId queryVal = queryVal |> fromASCIIBytes@@ -859,25 +1043,24 @@ parseRouteWithId :: forall controller application.- (?applicationContext :: ApplicationContext,- ?context :: RequestContext,- Controller controller,+ (+ ?request :: Request,+ ?respond :: Respond, CanRoute controller,+ Controller controller, InitControllerContext application, ?application :: application, Typeable application,- Data controller)- => Parser Application-parseRouteWithId = do- action <- parseRoute' @controller- pure (runAction' @application action)+ Typeable controller)+ => ControllerRoute application+parseRouteWithId = parseRoute @controller @application -catchAll :: forall action application. (?applicationContext :: ApplicationContext, ?context :: RequestContext, Controller action, InitControllerContext application, Typeable action, ?application :: application, Typeable application, Data action) => action -> Parser Application-catchAll action = do- string (ByteString.pack (actionPrefix @action))+catchAll :: forall action application. (Controller action, InitControllerContext application, Typeable action, ?application :: application, Typeable application, Data action) => action -> ControllerRoute application+catchAll action = ControllerRouteParser $ do+ string (Text.encodeUtf8 (actionPrefixText @action)) _ <- takeByteString pure (runAction' @application action)-{-# INLINABLE catchAll #-}+{-# INLINE catchAll #-} -- | This instances makes it possible to write @<a href={MyAction}/>@ in HSX instance {-# OVERLAPPABLE #-} (HasPath action) => ConvertibleStrings action Html5.AttributeValue where@@ -910,7 +1093,7 @@ parseIntegerId :: (Data idType) => ByteString -> Maybe idType parseIntegerId queryVal = let- rawValue :: Maybe Integer = readMay (cs queryVal :: String)+ rawValue :: Maybe Integer = readMaybe (cs queryVal :: String) in rawValue >>= Just . unsafeCoerce @@ -928,11 +1111,11 @@ -- > pure ShowPostAction { .. } -- Will parse the `postId` query in `/post?postId=09b545dd-9744-4ef8-87b8-8d227f4faa1e` ---routeParam :: (?context::RequestContext, ParamReader paramType) => ByteString -> paramType+routeParam :: (?request :: Request, ?respond :: Respond, ParamReader paramType) => ByteString -> paramType routeParam paramName =- let requestContext = ?context+ let customFields = TypeMap.insert ?request TypeMap.empty in- let ?context = FrozenControllerContext { requestContext = requestContext, customFields = mempty }+ let ?context = FrozenControllerContext { customFields } in param paramName -- | Display a better error when the user missed to pass an argument to an action.
@@ -1,136 +0,0 @@-{-|-Module: IHP.SchemaMigration-Description: Managing Database Migrations-Copyright: (c) digitally induced GmbH, 2020--}-module IHP.SchemaMigration where--import IHP.Prelude-import qualified System.Directory as Directory-import qualified Data.Text as Text-import qualified Data.Text.IO as Text-import IHP.ModelSupport hiding (withTransaction)-import qualified Data.Char as Char-import IHP.Log.Types-import IHP.EnvVar--data Migration = Migration- { revision :: Int- , migrationFile :: Text- } deriving (Show, Eq)--data MigrateOptions = MigrateOptions- { minimumRevision :: !(Maybe Int) -- ^ When deploying a fresh install of an existing app that has existing migrations, it might be useful to ignore older migrations as they're already part of the existing schema- }---- | Migrates the database schema to the latest version-migrate :: (?modelContext :: ModelContext) => MigrateOptions -> IO ()-migrate options = do- createSchemaMigrationsTable-- let minimumRevision = fromMaybe 0 options.minimumRevision-- openMigrations <- findOpenMigrations minimumRevision- forEach openMigrations runMigration---- | The sql statements contained in the migration file are executed. Then the revision is inserted into the @schema_migrations@ table.------ All queries are executed inside a database transaction to make sure that it can be restored when something goes wrong.-runMigration :: (?modelContext :: ModelContext) => Migration -> IO ()-runMigration migration@Migration { revision, migrationFile } = do- -- | User can specify migrations directory as environment variable (defaults to /Application/Migrations/...)- migrationFilePath <- migrationPath migration- migrationSql <- Text.readFile (cs migrationFilePath)-- let fullSql = [trimming|- BEGIN;- ${migrationSql};- INSERT INTO schema_migrations (revision) VALUES (?);- COMMIT;- |]- sqlExec (fromString . cs $ fullSql) [revision]-- pure ()---- | Creates the @schema_migrations@ table if it doesn't exist yet-createSchemaMigrationsTable :: (?modelContext :: ModelContext) => IO ()-createSchemaMigrationsTable = do- -- Hide this query from the log- withoutQueryLogging do- -- We don't use CREATE TABLE IF NOT EXISTS as adds a "NOTICE: relation schema_migrations already exists, skipping"- -- This sometimes confuses users as they don't know if the this is an error or not (it's not)- -- https://github.com/digitallyinduced/ihp/issues/818- maybeTableName :: Maybe Text <- sqlQueryScalar "SELECT (to_regclass('schema_migrations')) :: text" ()- let schemaMigrationTableExists = isJust maybeTableName-- unless schemaMigrationTableExists do- let ddl = "CREATE TABLE IF NOT EXISTS schema_migrations (revision BIGINT NOT NULL UNIQUE)"- _ <- sqlExec ddl ()- pure ()---- | Returns all migrations that haven't been executed yet. The result is sorted so that the oldest revision is first.-findOpenMigrations :: (?modelContext :: ModelContext) => Int -> IO [Migration]-findOpenMigrations !minimumRevision = do- let modelContext = ?modelContext- let ?modelContext = modelContext { logger = (modelContext.logger) { write = \_ -> pure ()} }-- migratedRevisions <- findMigratedRevisions- migrations <- findAllMigrations- migrations- |> filter (\Migration { revision } -> not (migratedRevisions |> includes revision))- |> filter (\Migration { revision } -> revision > minimumRevision)- |> pure---- | Returns all migration revisions applied to the database schema------ >>> findMigratedRevisions--- [ 1604850570, 1604850660 ]----findMigratedRevisions :: (?modelContext :: ModelContext) => IO [Int]-findMigratedRevisions = map (\[revision] -> revision) <$> sqlQuery "SELECT revision FROM schema_migrations ORDER BY revision" ()---- | Returns all migrations found in @Application/Migration@------ >>> findAllMigrations--- [ Migration { revision = 1604850570, migrationFile = "Application/Migration/1604850570-create-projects.sql" } ]------ The result is sorted so that the oldest revision is first.-findAllMigrations :: IO [Migration]-findAllMigrations = do- migrationDir <- detectMigrationDir- directoryFiles <- Directory.listDirectory (cs migrationDir)- directoryFiles- |> map cs- |> filter (\path -> ".sql" `isSuffixOf` path)- |> mapMaybe pathToMigration- |> sortBy (comparing revision)- |> pure---- | Given a path such as Application/Migrate/00-initial-migration.sql it returns a Migration------ Returns Nothing if the path is not following the usual migration file path convention.------ >>> pathToMigration "Application/Migration/1604850570-create-projects.sql"--- Migration { revision = 1604850570, migrationFile = "Application/Migration/1604850570-create-projects.sql" }----pathToMigration :: Text -> Maybe Migration-pathToMigration fileName = case revision of- Just revision -> Just Migration { migrationFile = fileName, revision }- Nothing -> Nothing- where- revision :: Maybe Int- revision = fileName- |> Text.split (not . Char.isDigit)- |> head- |> fmap textToInt- |> join--migrationPath :: Migration -> IO Text-migrationPath Migration { migrationFile } = do- migrationDir <- detectMigrationDir- pure (migrationDir <> migrationFile)--detectMigrationDir :: IO Text-detectMigrationDir =- envOrDefault "IHP_MIGRATION_DIR" "Application/Migration/"-
@@ -3,13 +3,11 @@ Description: Run scripts inside the framework context, but outside of the usual web request response lifecycle Copyright: (c) digitally induced GmbH, 2020 -}-module IHP.ScriptSupport (runScript, Script, module IHP.FrameworkConfig) where+module IHP.ScriptSupport (runScript, runDevScript, Script, module IHP.FrameworkConfig) where import IHP.Prelude import IHP.FrameworkConfig-import IHP.ModelSupport-import Control.Exception (finally)-import IHP.Log (Logger(cleanup))+import IHP.ModelSupport (withModelContext) import Main.Utf8 (withUtf8) -- | A script is just an IO action which requires a database connection and framework config@@ -18,10 +16,28 @@ -- | Initializes IHP and then runs the script inside the framework context runScript :: ConfigBuilder -> Script -> IO () runScript configBuilder taskMain = withUtf8 do- frameworkConfig@FrameworkConfig { environment, dbPoolIdleTime, dbPoolMaxConnections, databaseUrl, logger } <- buildFrameworkConfig configBuilder- modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections databaseUrl logger-- let ?modelContext = modelContext- let ?context = frameworkConfig- taskMain `finally` cleanup logger+ withFrameworkConfig configBuilder \frameworkConfig -> do+ withModelContext frameworkConfig.databaseUrl frameworkConfig.logger \modelContext -> do+ let ?modelContext = modelContext+ let ?context = frameworkConfig+ taskMain {-# INLINABLE runScript #-}++-- | Run a script interactively from GHCi using the default IHP config.+--+-- This reads DATABASE_URL from the environment (set by devenv) and uses+-- default settings for everything else. For custom config, use 'runScript' instead.+--+-- __Example:__ Run a script module from GHCi:+--+-- > import IHP.ScriptSupport+-- > :l Application/Script/HelloWorld.hs+-- > runDevScript run+--+-- __Example:__ Run inline script code:+--+-- > import IHP.ScriptSupport+-- > runDevScript do { users <- query @User |> fetch; forEach users \user -> putStrLn user.name }+--+runDevScript :: Script -> IO ()+runDevScript = runScript ihpDefaultConfig
@@ -1,32 +1,31 @@ {-# LANGUAGE IncoherentInstances #-} -module IHP.Server (run, application) where+module IHP.Server (run, application, initSessionMiddleware, initMiddlewareStack) where import IHP.Prelude import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp.Systemd as Systemd import Network.Wai import Network.Wai.Middleware.MethodOverridePost (methodOverridePost)-import Network.Wai.Session (withSession)-import Network.Wai.Session.ClientSession (clientsessionStore)+import Network.Wai.Session.Maybe (withSession)+import Network.Wai.Session.ClientSession.Deferred (clientsessionStore) import qualified Network.Wai.Middleware.HealthCheckEndpoint as HealthCheckEndpoint import qualified Web.ClientSession as ClientSession import IHP.Controller.Session (sessionVaultKey)-import IHP.ApplicationContext import qualified IHP.Environment as Env import qualified IHP.PGListener as PGListener import IHP.FrameworkConfig+import IHP.ModelSupport (withModelContext) import IHP.RouterSupport (frontControllerToWAIApp, FrontController)-import qualified IHP.AutoRefresh as AutoRefresh-import qualified IHP.AutoRefresh.Types as AutoRefresh+import IHP.AutoRefresh (AutoRefreshWSApp) import qualified IHP.Job.Runner as Job import qualified IHP.Job.Types as Job import qualified Data.ByteString.Char8 as ByteString import qualified Network.Wai.Middleware.Cors as Cors import qualified Network.Wai.Middleware.Approot as Approot-import qualified Control.Exception as Exception+import qualified Network.Wai.Middleware.AssetPath as AssetPath -import qualified System.Directory as Directory+import qualified System.Directory.OsPath as Directory import qualified GHC.IO.Encoding as IO import qualified System.IO as IO @@ -35,9 +34,22 @@ import qualified IHP.EnvVar as EnvVar import qualified Network.Wreq as Wreq import qualified Data.Function as Function+import IHP.RequestVault hiding (requestBodyMiddleware)+import IHP.Controller.Response (responseHeadersVaultKey)+import IHP.ControllerSupport (rlsContextVaultKey)+import IHP.PageHead.Types+import IHP.Modal.Types (modalContainerVaultKey) import IHP.Controller.NotFound (handleNotFound)+import IHP.Static (staticRouteShortcut)+import Wai.Request.Params.Middleware (requestBodyMiddleware) import Paths_ihp (getDataFileName)+import IHP.Controller.Layout (viewLayoutMiddleware)+import qualified Network.Socket as Socket+import qualified System.Environment as Env+import qualified Text.Read as Read+import qualified System.Posix.IO as Posix+import System.Posix.Types (Fd(..)) run :: (FrontController RootApplication, Job.Worker RootApplication) => ConfigBuilder -> IO () run configBuilder = do@@ -46,34 +58,24 @@ IO.setLocaleEncoding IO.utf8 withFrameworkConfig configBuilder \frameworkConfig -> do- modelContext <- IHP.FrameworkConfig.initModelContext frameworkConfig-- approotMiddleware <- Approot.envFallback-- withInitalizers frameworkConfig modelContext do- PGListener.withPGListener modelContext \pgListener -> do- autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener)+ withModelContext frameworkConfig.databaseUrl frameworkConfig.logger \modelContext -> do+ withInitalizers frameworkConfig modelContext do+ PGListener.withPGListener frameworkConfig.databaseUrl frameworkConfig.logger \pgListener -> do+ let ?modelContext = modelContext - let ?modelContext = modelContext- let ?applicationContext = ApplicationContext { modelContext = ?modelContext, autoRefreshServer, frameworkConfig, pgListener }+ middleware <- initMiddlewareStack frameworkConfig modelContext (Just pgListener)+ staticApp <- initStaticApp frameworkConfig+ let requestLoggerMiddleware = frameworkConfig.requestLoggerMiddleware - sessionMiddleware <- initSessionMiddleware frameworkConfig- staticApp <- initStaticApp frameworkConfig- let corsMiddleware = initCorsMiddleware frameworkConfig- let requestLoggerMiddleware = frameworkConfig.requestLoggerMiddleware- let CustomMiddleware customMiddleware = frameworkConfig.customMiddleware+ useSystemd <- EnvVar.envOrDefault "IHP_SYSTEMD" False - useSystemd <- EnvVar.envOrDefault "IHP_SYSTEMD" False+ let fullApp = middleware $ application staticApp requestLoggerMiddleware+ let staticShortcut = staticRouteShortcut staticApp fullApp - withBackgroundWorkers pgListener frameworkConfig- . runServer frameworkConfig useSystemd- . (if useSystemd then HealthCheckEndpoint.healthCheck else Function.id)- . customMiddleware- . corsMiddleware- . methodOverridePost- . sessionMiddleware- . approotMiddleware- $ application staticApp requestLoggerMiddleware+ withBackgroundWorkers pgListener frameworkConfig+ . runServer frameworkConfig useSystemd+ . (if useSystemd then HealthCheckEndpoint.healthCheck else Function.id)+ $ staticShortcut {-# INLINABLE run #-} @@ -94,7 +96,12 @@ -- - In production mode: We cache files forever. IHP's 'assetPath' helper will add a hash to files to cache bust when something has changed. initStaticApp :: FrameworkConfig -> IO Application initStaticApp frameworkConfig = do- frameworkStaticDir <- getDataFileName "static"+ frameworkStaticDir <- do+ ihpStaticOverride <- EnvVar.envOrNothing "IHP_STATIC"+ case ihpStaticOverride of+ Just dir -> pure dir+ Nothing -> getDataFileName "static"+ appStaticDir <- EnvVar.envOrDefault "APP_STATIC" "static/" let maxAge = case frameworkConfig.environment of Env.Development -> Static.MaxAgeSeconds 0@@ -104,7 +111,7 @@ { Static.ss404Handler = Just (frameworkConfig.requestLoggerMiddleware handleNotFound) , Static.ssMaxAge = maxAge }- appSettings = (Static.defaultWebAppSettings "static/")+ appSettings = (Static.defaultWebAppSettings appStaticDir) { Static.ss404Handler = Just (Static.staticApp frameworkSettings) , Static.ssMaxAge = maxAge }@@ -116,11 +123,17 @@ let path = "Config/client_session_key.aes" hasSessionSecretEnvVar <- EnvVar.hasEnvVar "IHP_SESSION_SECRET"+ hasSessionSecretFileEnvVar <- EnvVar.hasEnvVar "IHP_SESSION_SECRET_FILE" doesConfigDirectoryExist <- Directory.doesDirectoryExist "Config" store <- clientsessionStore <$>- if hasSessionSecretEnvVar || not doesConfigDirectoryExist- then ClientSession.getKeyEnv "IHP_SESSION_SECRET"- else ClientSession.getKey path+ if hasSessionSecretFileEnvVar+ then do+ path <- EnvVar.env "IHP_SESSION_SECRET_FILE"+ ClientSession.getKey path+ else+ if hasSessionSecretEnvVar || not doesConfigDirectoryExist+ then ClientSession.getKeyEnv "IHP_SESSION_SECRET"+ else ClientSession.getKey path let sessionMiddleware :: Middleware = withSession store "SESSION" sessionCookie sessionVaultKey pure sessionMiddleware @@ -129,19 +142,69 @@ Just corsResourcePolicy -> Cors.cors (const (Just corsResourcePolicy)) Nothing -> id -application :: (FrontController RootApplication, ?applicationContext :: ApplicationContext) => Application -> Middleware -> Application+-- | Initialize the complete middleware stack+--+-- Pass Nothing for PGListener in tests+-- Pass Just pgListener in production for full functionality+initMiddlewareStack :: FrameworkConfig -> ModelContext -> Maybe PGListener.PGListener -> IO Middleware+initMiddlewareStack frameworkConfig modelContext maybePgListener = do+ sessionMiddleware <- initSessionMiddleware frameworkConfig+ approotMiddleware <- Approot.envFallback+ assetPathMiddleware <- AssetPath.assetPathFromEnvMiddleware "IHP_ASSET_VERSION" "IHP_ASSET_BASEURL"++ let corsMiddleware = initCorsMiddleware frameworkConfig+ let CustomMiddleware customMiddleware = frameworkConfig.customMiddleware+ let pgListenerMw = maybe id pgListenerMiddleware maybePgListener++ let responseHeadersMiddleware = insertNewIORefVaultMiddleware responseHeadersVaultKey []+ let rlsContextMiddleware = insertNewIORefVaultMiddleware rlsContextVaultKey Nothing+ let modalMiddleware = insertNewIORefVaultMiddleware modalContainerVaultKey Nothing+ let pageHeadMiddleware = insertNewIORefVaultMiddleware pageHeadVaultKey emptyPageHeadState++ pure $+ customMiddleware+ . corsMiddleware+ . methodOverridePost+ . sessionMiddleware+ . approotMiddleware+ . viewLayoutMiddleware+ . responseHeadersMiddleware+ . rlsContextMiddleware+ . pageHeadMiddleware+ . modalMiddleware+ . modelContextMiddleware modelContext+ . frameworkConfigMiddleware frameworkConfig+ . requestBodyMiddleware frameworkConfig.parseRequestBodyOptions+ . pgListenerMw+ . assetPathMiddleware++application :: (FrontController RootApplication) => Application -> Middleware -> Application application staticApp middleware request respond = do- frontControllerToWAIApp @RootApplication @AutoRefresh.AutoRefreshWSApp middleware RootApplication staticApp request respond+ frontControllerToWAIApp @RootApplication @AutoRefreshWSApp middleware RootApplication staticApp request respond {-# INLINABLE application #-} -runServer :: (?applicationContext :: ApplicationContext) => FrameworkConfig -> Bool -> Application -> IO ()-runServer config@FrameworkConfig { environment = Env.Development, appPort } useSystemd = Warp.runSettings $- Warp.defaultSettings- |> Warp.setBeforeMainLoop (do- ByteString.putStrLn "Server started"- IO.hFlush IO.stdout- )- |> Warp.setPort appPort+runServer :: FrameworkConfig -> Bool -> Application -> IO ()+runServer config@FrameworkConfig { environment = Env.Development, appPort } useSystemd = \app -> do+ let warpSettings = Warp.defaultSettings+ |> Warp.setBeforeMainLoop (do+ ByteString.putStrLn "Server started"+ IO.hFlush IO.stdout+ )+ |> Warp.setPort appPort++ -- Check if we have a socket FD passed from the dev server+ -- This enables seamless transitions during app restarts+ socketFdEnv <- Env.lookupEnv "IHP_SOCKET_FD"+ case socketFdEnv of+ Just fdStr | Just fd <- Read.readMaybe fdStr -> do+ -- Duplicate the FD so that when the Socket is garbage collected,+ -- it closes the duplicate rather than the original FD.+ -- This preserves the original FD for future app restarts.+ dupFd <- Posix.dup (Fd (fromIntegral (fd :: Int)))+ socket <- Socket.mkSocket (fromIntegral dupFd)+ Warp.runSettingsSocket warpSettings socket app+ _ ->+ Warp.runSettings warpSettings app runServer FrameworkConfig { environment = Env.Production, appPort, exceptionTracker } useSystemd = let warpSettings = Warp.defaultSettings
@@ -0,0 +1,21 @@+module IHP.Static (staticRouteShortcut) where++import Network.Wai+import qualified Data.ByteString.Char8 as ByteString++-- | Routes requests with a @/static/@ prefix directly to the static file server,+-- bypassing the full middleware stack (session, CORS, auto-refresh, etc.).+-- All other requests go through the normal middleware pipeline.+--+-- This improves performance for static assets since they only need to be+-- read from disk.+staticRouteShortcut :: Application -> Application -> Application+staticRouteShortcut staticApp fallbackApp request respond =+ case request.pathInfo of+ ("static":rest) ->+ let strippedRequest = request+ { pathInfo = rest+ , rawPathInfo = ByteString.drop (ByteString.length "/static") request.rawPathInfo+ }+ in staticApp strippedRequest respond+ _ -> fallbackApp request respond
@@ -9,18 +9,16 @@ import Data.ByteString.Builder (toLazyByteString) import qualified Data.ByteString.Lazy as LBS import qualified Data.Vault.Lazy as Vault-import qualified Network.HTTP.Types.Status as HTTP+import qualified Network.HTTP.Types as HTTP import Network.Wai import Network.Wai.Internal (ResponseReceived (..)) import Network.Wai.Parse (Param (..)) -import IHP.ApplicationContext (ApplicationContext (..))-import qualified IHP.AutoRefresh.Types as AutoRefresh-import IHP.Controller.RequestContext (RequestBody (..), RequestContext (..))+import Wai.Request.Params.Middleware (Respond) import IHP.ControllerSupport (InitControllerContext, Controller, runActionWithNewContext)-import IHP.FrameworkConfig (ConfigBuilder (..), FrameworkConfig (..))+import IHP.FrameworkConfig (ConfigBuilder (..), FrameworkConfig (..), RootApplication (..)) import qualified IHP.FrameworkConfig as FrameworkConfig-import IHP.ModelSupport (createModelContext, Id')+import IHP.ModelSupport (createModelContext, withModelContext, Id') import IHP.Prelude import IHP.Log.Types import IHP.Job.Types@@ -28,49 +26,114 @@ import qualified Data.Text as Text import qualified Network.Wai as Wai import qualified IHP.LoginSupport.Helper.Controller as Session-import qualified Network.Wai.Session+import qualified Network.Wai.Session.Maybe import qualified Data.Serialize as Serialize-import qualified Control.Exception as Exception-import qualified IHP.PGListener as PGListener import IHP.Controller.Session (sessionVaultKey)-import qualified Network.Wai.Middleware.Approot as Approot-import qualified Network.Wai.Test as WaiTest+import IHP.Server (initMiddlewareStack)+import qualified IHP.Server as Server+import IHP.Controller.NotFound (handleNotFound)+import IHP.RouterSupport (FrontController)+import qualified IHP.PGListener as PGListener -type ContextParameters application = (?applicationContext :: ApplicationContext, ?context :: RequestContext, ?modelContext :: ModelContext, ?application :: application, InitControllerContext application, ?mocking :: MockContext application)+type ContextParameters application = (?request :: Request, ?respond :: Respond, ?modelContext :: ModelContext, ?application :: application, InitControllerContext application, ?mocking :: MockContext application) data MockContext application = InitControllerContext application => MockContext { modelContext :: ModelContext- , requestContext :: RequestContext- , applicationContext :: ApplicationContext+ , frameworkConfig :: FrameworkConfig+ , mockRequest :: Request+ , mockRespond :: Respond , application :: application+ , pgListener :: Maybe PGListener.PGListener } +-- | Run a request through the test middleware stack.+-- This applies the same middlewares that IHP.Server uses.+-- Used for initial setup only - actual request params are handled in callActionWithParams.+runTestMiddlewares :: FrameworkConfig -> ModelContext -> Maybe PGListener.PGListener -> Request -> IO Request+runTestMiddlewares frameworkConfig modelContext maybePgListener baseRequest = do+ -- Capture the modified request after running through middlewares+ resultRef <- newIORef baseRequest+ let captureApp req respond = do+ writeIORef resultRef req+ respond (responseLBS HTTP.status200 [] "")++ middlewareStack <- initMiddlewareStack frameworkConfig modelContext maybePgListener++ -- Run request through middleware stack+ _ <- middlewareStack captureApp baseRequest (\_ -> pure ResponseReceived)++ readIORef resultRef++{-# DEPRECATED mockContextNoDatabase "Use withMockContext instead for bracket-style resource management" #-} mockContextNoDatabase :: (InitControllerContext application) => application -> ConfigBuilder -> IO (MockContext application) mockContextNoDatabase application configBuilder = do- frameworkConfig@(FrameworkConfig {dbPoolMaxConnections, dbPoolIdleTime, databaseUrl}) <- FrameworkConfig.buildFrameworkConfig configBuilder- let databaseConnection = undefined- logger <- newLogger def { level = Warn } -- don't log queries- modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections databaseUrl logger-- let sessionVault = Vault.insert sessionVaultKey mempty Vault.empty- pgListener <- PGListener.init modelContext- autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener)- let applicationContext = ApplicationContext { modelContext = modelContext, autoRefreshServer, frameworkConfig, pgListener }+ frameworkConfig@(FrameworkConfig {databaseUrl}) <- FrameworkConfig.buildFrameworkConfig configBuilder+ logger <- newLogger (def :: LoggerSettings) { level = Warn } -- don't log queries+ modelContext <- createModelContext databaseUrl logger - let requestContext = RequestContext- { request = defaultRequest {vault = sessionVault}- , requestBody = FormBody [] []- , respond = \resp -> pure ResponseReceived- , frameworkConfig = frameworkConfig }+ -- Start with a minimal request - the middleware stack will set up session, etc.+ let baseRequest = defaultRequest+ let pgListener = Nothing+ mockRequest <- runTestMiddlewares frameworkConfig modelContext pgListener baseRequest+ let mockRespond = const (pure ResponseReceived) pure MockContext{..} +-- | Bracket-style mock context creation with proper resource cleanup.+--+-- Uses 'withModelContext' to ensure the database pool is released when done.+-- Prefer this over 'mockContextNoDatabase'.+--+-- __Example:__ Use with hspec's 'aroundAll':+--+-- > tests :: Spec+-- > tests = aroundAll (withMockContext WebApplication config) do+-- > it "should work" $ withContext do+-- > ...+--+withMockContext :: (InitControllerContext application) => application -> ConfigBuilder -> (MockContext application -> IO a) -> IO a+withMockContext application configBuilder action =+ FrameworkConfig.withFrameworkConfig configBuilder \frameworkConfig -> do+ withModelContext frameworkConfig.databaseUrl frameworkConfig.logger \modelContext -> do+ PGListener.withPGListener frameworkConfig.databaseUrl frameworkConfig.logger \pgListener' -> do+ let baseRequest = defaultRequest+ let pgListener = Just pgListener'+ mockRequest <- runTestMiddlewares frameworkConfig modelContext pgListener baseRequest+ let mockRespond = const (pure ResponseReceived)+ action MockContext{..}++-- | Build a WAI 'Application' from a 'MockContext' for use with @runSession@.+initTestApplication :: (FrontController RootApplication) => MockContext application -> IO Application+initTestApplication MockContext { frameworkConfig, modelContext, pgListener } = do+ middleware <- initMiddlewareStack frameworkConfig modelContext pgListener+ pure (middleware $ Server.application handleNotFound (\app -> app))++-- | Combines 'withMockContext' and 'initTestApplication' into a single bracket.+--+-- __Example:__ Use with hspec's 'aroundAll':+--+-- > tests :: Spec+-- > tests = aroundAll (withMockContextAndApp WebApplication config) do+-- > it "should work" $ withContextAndApp \application -> do+-- > runSession (testGet "/foo") application >>= assertSuccess "bar"+--+withMockContextAndApp :: (InitControllerContext application, FrontController RootApplication) => application -> ConfigBuilder -> ((MockContext application, Application) -> IO a) -> IO a+withMockContextAndApp application configBuilder action =+ withMockContext application configBuilder \ctx -> do+ app <- initTestApplication ctx+ action (ctx, app)++-- | Like 'withContext' but for specs using 'withMockContextAndApp'.+-- The WAI 'Application' is passed to the callback.+withContextAndApp :: (ContextParameters application => Application -> IO a) -> (MockContext application, Application) -> IO a+withContextAndApp action (ctx, app) = withContext (action app) ctx+ -- | Run a IO action, setting implicit params based on supplied mock context withContext :: (ContextParameters application => IO a) -> MockContext application -> IO a withContext action mocking@MockContext{..} = let ?modelContext = modelContext- ?context = requestContext- ?applicationContext = applicationContext+ ?request = mockRequest+ ?respond = mockRespond ?application = application ?mocking = mocking in do@@ -90,19 +153,44 @@ -- callActionWithParams :: forall application controller. (Controller controller, ContextParameters application, Typeable application, Typeable controller) => controller -> [Param] -> IO Response callActionWithParams controller params = do- approotMiddleware <- Approot.envFallback- let ihpWaiApp request respond = do- let requestContextWithOverridenRespond = ?context { respond, request, requestBody = FormBody params [] }- let ?context = requestContextWithOverridenRespond- runActionWithNewContext controller+ let MockContext { frameworkConfig, modelContext } = ?mocking - allMiddlewares app = approotMiddleware app+ -- Build request with real form body (let middleware parse it)+ requestBody <- newIORef (HTTP.renderSimpleQuery False params)+ let readBody = atomicModifyIORef requestBody (\body -> ("", body))+ let baseRequest = (Wai.setRequestBodyChunks readBody ?request)+ { Wai.requestMethod = "POST"+ , Wai.requestHeaders = (HTTP.hContentType, "application/x-www-form-urlencoded") : filter ((/= HTTP.hContentType) . fst) (Wai.requestHeaders ?request)+ } - simpleResponse <- WaiTest.withSession (allMiddlewares ihpWaiApp) do- WaiTest.request ?context.request+ -- Capture the response+ responseRef <- newIORef Nothing+ let captureRespond response = do+ writeIORef responseRef (Just response)+ pure ResponseReceived - pure $ Wai.responseLBS simpleResponse.simpleStatus simpleResponse.simpleHeaders simpleResponse.simpleBody+ -- Check if withUser set a mock session that we need to preserve+ let mockSession = Vault.lookup sessionVaultKey (Wai.vault ?request) + -- Create the controller app+ let controllerApp req respond = do+ -- Restore mock session from withUser if it was set+ let req' = case mockSession of+ Just session -> req { Wai.vault = Vault.insert sessionVaultKey session (Wai.vault req) }+ Nothing -> req+ let ?request = req'+ let ?respond = respond+ runActionWithNewContext controller++ -- Run through middleware stack (like the real server does)+ let MockContext { pgListener } = ?mocking+ middlewareStack <- initMiddlewareStack frameworkConfig modelContext pgListener+ _ <- middlewareStack controllerApp baseRequest captureRespond++ readIORef responseRef >>= \case+ Just response -> pure response+ Nothing -> error "callActionWithParams: No response was returned by the controller"+ -- | Run a Job in a mock environment -- -- __Example:__@@ -118,7 +206,7 @@ -- callJob :: forall application job. (ContextParameters application, Typeable application, Job job) => job -> IO () callJob job = do- let frameworkConfig = ?context.frameworkConfig+ let frameworkConfig = ?request.frameworkConfig let ?context = frameworkConfig perform job @@ -178,20 +266,19 @@ -- withUser :: forall user application userId result. ( ?mocking :: MockContext application- , ?applicationContext :: ApplicationContext- , ?context :: RequestContext+ , ?request :: Request+ , ?respond :: Respond , Serialize.Serialize userId , HasField "id" user userId , KnownSymbol (GetModelName user)- ) => user -> ((?context :: RequestContext) => IO result) -> IO result+ ) => user -> ((?request :: Request, ?respond :: Respond) => IO result) -> IO result withUser user callback =- let ?context = newContext+ let ?request = newRequest in callback where- newContext = ?context { request = newRequest }- newRequest = request { Wai.vault = newVault }+ newRequest = currentRequest { Wai.vault = newVault } - newSession :: Network.Wai.Session.Session IO ByteString ByteString+ newSession :: Network.Wai.Session.Maybe.Session IO ByteString ByteString newSession = (lookupSession, insertSession) lookupSession key = if key == sessionKey@@ -200,8 +287,8 @@ insertSession key value = pure () - newVault = Vault.insert sessionVaultKey newSession (Wai.vault request)- RequestContext { request } = ?mocking.requestContext+ newVault = Vault.insert sessionVaultKey newSession (Wai.vault currentRequest)+ currentRequest = ?request sessionValue = Serialize.encode (user.id) sessionKey = cs (Session.sessionKey @user)
@@ -1,8 +1,15 @@ module IHP.ValidationSupport.Types where -import IHP.Prelude+import Prelude+import Data.Text (Text) import qualified Data.Text as Text-import IHP.ModelSupport (Violation (..))+import Data.Proxy (Proxy)+import GHC.TypeLits (KnownSymbol, symbolVal)+import GHC.Records (HasField(..))+import IHP.HaskellSupport (SetField(..), modify, (|>))+import Data.String.Conversions (cs)+import IHP.ModelSupport.Types (Violation(..), MetaBag(..))+import IHP.ModelSupport () -- for SetField instances on MetaBag import Text.Blaze.Html5 (Html) import qualified Text.Blaze.Html.Renderer.Text as Blaze import qualified Data.List as List
@@ -135,6 +135,7 @@ withCustomErrorMessage errorMessage validator value = case validator value of Failure _ -> Failure errorMessage+ FailureHtml _ -> Failure errorMessage Success -> Success {-# INLINABLE withCustomErrorMessage #-}
@@ -5,11 +5,12 @@ ) where import IHP.Prelude-import Database.PostgreSQL.Simple.ToField import IHP.ModelSupport import IHP.ValidationSupport.Types import IHP.QueryBuilder import IHP.Fetch+import IHP.Hasql.FromRow (FromRowHasql)+import Hasql.Implicits.Encoders (DefaultParamEncoder) -- | Validates that e.g. an email (or another field) is unique across all users before inserting. --@@ -31,12 +32,12 @@ validateIsUnique :: forall field model savedModel fieldValue modelId savedModelId. ( savedModel ~ NormalizeModel model , ?modelContext :: ModelContext- , FromRow savedModel+ , FromRowHasql savedModel , KnownSymbol field , HasField field model fieldValue , HasField field savedModel fieldValue , KnownSymbol (GetTableName savedModel)- , ToField fieldValue+ , DefaultParamEncoder fieldValue , EqOrIsOperator fieldValue , HasField "meta" model MetaBag , SetField "meta" model MetaBag@@ -72,12 +73,12 @@ validateIsUniqueCaseInsensitive :: forall field model savedModel fieldValue modelId savedModelId. ( savedModel ~ NormalizeModel model , ?modelContext :: ModelContext- , FromRow savedModel+ , FromRowHasql savedModel , KnownSymbol field , HasField field model fieldValue , HasField field savedModel fieldValue , KnownSymbol (GetTableName savedModel)- , ToField fieldValue+ , DefaultParamEncoder fieldValue , EqOrIsOperator fieldValue , HasField "meta" model MetaBag , SetField "meta" model MetaBag@@ -95,12 +96,12 @@ validateIsUniqueCaseAware :: forall field model savedModel fieldValue modelId savedModelId. ( savedModel ~ NormalizeModel model , ?modelContext :: ModelContext- , FromRow savedModel+ , FromRowHasql savedModel , KnownSymbol field , HasField field model fieldValue , HasField field savedModel fieldValue , KnownSymbol (GetTableName savedModel)- , ToField fieldValue+ , DefaultParamEncoder fieldValue , EqOrIsOperator fieldValue , HasField "meta" model MetaBag , SetField "meta" model MetaBag@@ -141,12 +142,11 @@ withCustomErrorMessageIO :: forall field model savedModel fieldValue modelId savedModelId. ( savedModel ~ NormalizeModel model , ?modelContext :: ModelContext- , FromRow savedModel , KnownSymbol field , HasField field model fieldValue , HasField field savedModel fieldValue , KnownSymbol (GetTableName savedModel)- , ToField fieldValue+ , DefaultParamEncoder fieldValue , EqOrIsOperator fieldValue , HasField "meta" model MetaBag , SetField "meta" model MetaBag
@@ -3,1128 +3,12 @@ Description: Adds support for bootstrap, tailwind, etc. to IHP Copyright: (c) digitally induced GmbH, 2020 -}-module IHP.View.CSSFramework where--import IHP.Prelude-import IHP.FlashMessages.Types-import qualified Text.Blaze.Html5 as Blaze-import IHP.HSX.QQ (hsx)-import IHP.HSX.ToHtml ()-import IHP.View.Types-import IHP.View.Classes--import IHP.ModelSupport-import IHP.Breadcrumb.Types-import IHP.Pagination.Helpers-import IHP.Pagination.Types----- | Provides an unstyled CSSFramework------ This way we can later add more properties to the CSSFramework without having to update all the CSS Frameworks manually-instance Default CSSFramework where- def = CSSFramework- {- styledFlashMessage = \cssFramework -> \case- SuccessFlashMessage message -> [hsx|<div>{message}</div>|]- ErrorFlashMessage message -> [hsx|<div>{message}</div>|]- , styledFlashMessages- , styledFormField- , styledTextFormField- , styledTextareaFormField- , styledCheckboxFormField- , styledSelectFormField- , styledRadioFormField- , styledFormGroup- , styledSubmitButton- , styledSubmitButtonClass- , styledFormFieldHelp- , styledInputClass- , styledInputInvalidClass- , styledFormGroupClass- , styledValidationResult- , styledValidationResultClass- , styledPagination- , styledPaginationPageLink- , styledPaginationDotDot- , styledPaginationItemsPerPageSelector- , styledPaginationLinkPrevious- , styledPaginationLinkNext- , styledBreadcrumb- , styledBreadcrumbItem- }- where- styledFlashMessages cssFramework flashMessages = forEach flashMessages (cssFramework.styledFlashMessage cssFramework)-- styledFormField :: CSSFramework -> FormField -> Blaze.Html- styledFormField cssFramework@CSSFramework {styledValidationResult, styledTextFormField, styledCheckboxFormField, styledSelectFormField, styledRadioFormField, styledTextareaFormField} formField =- formGroup renderInner- where- renderInner = case formField.fieldType of- TextInput -> styledTextFormField cssFramework "text" formField validationResult- NumberInput -> styledTextFormField cssFramework "number" formField validationResult- UrlInput -> styledTextFormField cssFramework "url" formField validationResult- PasswordInput -> styledTextFormField cssFramework "password" formField validationResult- ColorInput -> styledTextFormField cssFramework "color" formField validationResult- EmailInput -> styledTextFormField cssFramework "email" formField validationResult- DateInput -> styledTextFormField cssFramework "date" formField validationResult- DateTimeInput -> styledTextFormField cssFramework "datetime-local" formField validationResult- CheckboxInput -> styledCheckboxFormField cssFramework formField validationResult- HiddenInput -> styledTextFormField cssFramework "hidden" formField validationResult- TextareaInput -> styledTextareaFormField cssFramework formField validationResult- SelectInput {} -> styledSelectFormField cssFramework formField validationResult- RadioInput {} -> styledRadioFormField cssFramework formField validationResult- FileInput -> styledTextFormField cssFramework "file" formField validationResult-- validationResult :: Blaze.Html- validationResult = unless formField.disableValidationResult (styledValidationResult cssFramework formField)-- -- | Wraps the input inside a @<div class="form-group">...</div>@ (unless @disableGroup = True@)- formGroup :: Blaze.Html -> Blaze.Html- formGroup renderInner = case formField of- FormField { disableGroup = True } -> renderInner- FormField { fieldInputId } -> styledFormGroup cssFramework fieldInputId renderInner--- styledFormGroup :: CSSFramework -> Text -> Blaze.Html -> Blaze.Html- styledFormGroup cssFramework@CSSFramework {styledFormGroupClass} fieldInputId renderInner =- [hsx|<div class={styledFormGroupClass} id={"form-group-" <> fieldInputId}>{renderInner}</div>|]-- styledCheckboxFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledCheckboxFormField cssFramework@CSSFramework {styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult = do- [hsx|<div class="form-check">{element}</div>|]- where- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- -- If the checkbox is checked off, the browser will not send the parameter as part of the form.- -- This will then make it impossible to set a field to False using a checkbox.- -- For that we add the "hidden" input type.- theInput = [hsx|- <input- type="checkbox"- name={fieldName}- class={classes ["form-check-input", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- id={fieldInputId}- checked={fieldValue == "yes"}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- />-- <input type="hidden" name={fieldName} value={inputValue False} />- |]--- element = if disableLabel- then [hsx|<div>- {theInput}- {validationResult}- {helpText}- </div>- |]- else [hsx|- {theInput}- <label- class={classes [("form-check-label", labelClass == ""), (labelClass, labelClass /= "")]}- for={fieldInputId}- >- {fieldLabel}- </label>-- {validationResult}- {helpText}- |]-- styledTextFormField :: CSSFramework -> Text -> FormField -> Blaze.Html -> Blaze.Html- styledTextFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} inputType formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =- [hsx|- {label}- <input- type={inputType}- name={fieldName}- placeholder={placeholder}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- value={maybeValue}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- />-- {validationResult}- {helpText}- |]- where- label = unless (disableLabel || null fieldLabel) [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField- -- If there's no value, then we want to hide the "value" attribute.- maybeValue = if fieldValue == "" then Nothing else Just fieldValue-- styledSelectFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledSelectFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =- [hsx|- {label}- <select- name={fieldName}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- value={fieldValue}- disabled={disabled}- required={required}- autofocus={autofocus}- {...additionalAttributes}- >- <option value="" selected={not isValueSelected} disabled={True}>{placeholder}</option>- {forEach (options fieldType) (getOption)}- </select>-- {validationResult}- {helpText}- |]- where- label = unless disableLabel [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- isValueSelected = any (\(_, optionValue) -> optionValue == fieldValue) (options fieldType)-- -- Get a single option.- getOption (optionLabel, optionValue) = [hsx|- <option value={optionValue} selected={optionValue == fieldValue}>- {optionLabel}- </option>- |]--- styledRadioFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledRadioFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =- [hsx|- {label}- <fieldset>- {forEach (options fieldType) (getRadio)}- </fieldset>-- {validationResult}- {helpText}- |]- where- label = unless disableLabel [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- -- Get a single radio button.- getRadio (optionLabel, optionValue) = [hsx|- <div class="form-check">- <input- class={classes ["form-check-input", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- type="radio"- id={optionId}- name={fieldName}- value={optionValue}- checked={optionValue == fieldValue}- disabled={disabled}- required={required}- autofocus={autofocus}- {...additionalAttributes}- />- {label}- </div>- |]- where- optionId = fieldInputId <> "_" <> optionValue- label = unless disableLabel [hsx|<label class={classes ["form-check-label", (labelClass, labelClass /= "")]} for={optionId}>{optionLabel}</label>|]-- styledTextareaFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledTextareaFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =- [hsx|- {label}- <textarea- name={fieldName}- placeholder={placeholder}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- >{fieldValue}</textarea>{validationResult}{helpText}|]- where- label = unless (disableLabel || null fieldLabel) [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- styledValidationResult :: CSSFramework -> FormField -> Blaze.Html- styledValidationResult cssFramework formField@FormField { validatorResult = Just violation } =- let- className :: Text = cssFramework.styledValidationResultClass- message = case violation of- TextViolation text -> [hsx|{text}|]- HtmlViolation html -> Blaze.preEscapedToHtml html- in- [hsx|<div class={className}>{message}</div>|]- styledValidationResult _ _ = mempty-- styledValidationResultClass = ""-- styledSubmitButton cssFramework SubmitButton { label, buttonClass, buttonDisabled } =- let className :: Text = cssFramework.styledSubmitButtonClass- in [hsx|<button class={classes [(className, True), (buttonClass, not (null buttonClass))]} disabled={buttonDisabled} type="submit">{label}</button>|]-- styledInputClass _ _ = ""- styledInputInvalidClass _ _ = "invalid"-- styledFormGroupClass = ""-- styledFormFieldHelp _ FormField { helpText = "" } = mempty- styledFormFieldHelp _ FormField { helpText } = [hsx|<p>{helpText}</p>|]-- styledSubmitButtonClass = ""-- styledPagination :: CSSFramework -> PaginationView -> Blaze.Html- styledPagination _ paginationView =- [hsx|-- <div class="d-flex justify-content-md-center">- <nav aria-label="Page Navigator" class="mr-2">- <ul class="pagination">- {paginationView.linkPrevious}- {paginationView.pageDotDotItems}- {paginationView.linkNext}- </ul>- </nav>-- <div class="form-row">- <div class="col-auto mr-2">- <select class="custom-select" id="maxItemsSelect" onchange="window.location.href = this.options[this.selectedIndex].dataset.url">- {paginationView.itemsPerPageSelector}- </select>- </div>- </div>-- </div>- |]-- styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html- styledPaginationPageLink _ pagination@Pagination {currentPage} pageUrl pageNumber =- let- linkClass = classes ["page-item", ("active", pageNumber == currentPage)]- in- [hsx|<li class={linkClass}><a class="page-link" href={pageUrl}>{show pageNumber}</a></li>|]--- styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html- styledPaginationDotDot _ _ =- [hsx|<li class="page-item"><a class="page-link">…</a></li>|]-- styledPaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html- styledPaginationItemsPerPageSelector _ pagination@Pagination {pageSize} itemsPerPageUrl =- let- oneOption :: Int -> Blaze.Html- oneOption n = [hsx|<option value={show n} selected={n == pageSize} data-url={itemsPerPageUrl n}>{n} items per page</option>|]- in- [hsx|{forEach [10,20,50,100,200] oneOption}|]-- styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html- styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =- let- prevClass = classes ["page-item", ("disabled", not $ hasPreviousPage pagination)]- url = if hasPreviousPage pagination then pageUrl else "#"- in- [hsx|- <li class={prevClass}>- <a class="page-link" href={url} aria-label="Previous">- <span aria-hidden="true">«</span>- <span class="sr-only">Previous</span>- </a>- </li>- |]-- styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html- styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =- let- nextClass = classes ["page-item", ("disabled", not $ hasNextPage pagination)]- url = if hasNextPage pagination then pageUrl else "#"- in- [hsx|- <li class={nextClass}>- <a class="page-link" href={url} aria-label="Next">- <span aria-hidden="true">»</span>- <span class="sr-only">Next</span>- </a>- </li>- |]-- styledBreadcrumb :: CSSFramework -> [BreadcrumbItem]-> BreadcrumbsView -> Blaze.Html- styledBreadcrumb _ _ breadcrumbsView = [hsx|- <nav>- <ol class="breadcrumb">- {breadcrumbsView.breadcrumbItems}-- </ol>- </nav>- |]--- styledBreadcrumbItem :: CSSFramework -> [ BreadcrumbItem ]-> BreadcrumbItem -> Bool -> Blaze.Html- styledBreadcrumbItem _ breadcrumbItems breadcrumbItem@BreadcrumbItem {breadcrumbLabel, url} isLast =- let- breadcrumbsClasses = classes ["breadcrumb-item", ("active", isLast)]- in- case url of- Nothing -> [hsx|<li class={breadcrumbsClasses}>{breadcrumbLabel}</li>|]- Just url -> [hsx|<li class={breadcrumbsClasses}><a href={url}>{breadcrumbLabel}</a></li>|]----bootstrap :: CSSFramework-bootstrap = def- { styledFlashMessage- , styledSubmitButtonClass- , styledFormGroupClass- , styledFormFieldHelp- , styledInputClass- , styledInputInvalidClass- , styledValidationResultClass- }- where- styledFlashMessage _ (SuccessFlashMessage message) = [hsx|<div class="alert alert-success">{message}</div>|]- styledFlashMessage _ (ErrorFlashMessage message) = [hsx|<div class="alert alert-danger">{message}</div>|]-- styledInputClass _ FormField { fieldType = FileInput } = "form-control-file"- styledInputClass _ FormField {} = "form-control"- styledInputInvalidClass _ _ = "is-invalid"-- styledFormFieldHelp _ FormField { helpText = "" } = mempty- styledFormFieldHelp _ FormField { helpText } = [hsx|<small class="form-text">{helpText}</small>|]-- styledFormGroupClass = "mb-3"-- styledValidationResultClass = "invalid-feedback"-- styledSubmitButtonClass = "btn btn-primary"--bootstrap4 :: CSSFramework-bootstrap4 = def- { styledFlashMessage- , styledFormField- , styledTextFormField- , styledTextareaFormField- , styledCheckboxFormField- , styledSelectFormField- , styledFormGroup- , styledSubmitButton- , styledSubmitButtonClass- , styledFormFieldHelp- , styledInputClass- , styledInputInvalidClass- , styledFormGroupClass- , styledValidationResult- , styledValidationResultClass- , styledPagination- , styledPaginationPageLink- , styledPaginationDotDot- , styledPaginationItemsPerPageSelector- , styledPaginationLinkPrevious- , styledPaginationLinkNext- , styledBreadcrumb- , styledBreadcrumbItem- }- where- styledFlashMessage _ (SuccessFlashMessage message) = [hsx|<div class="alert alert-success">{message}</div>|]- styledFlashMessage _ (ErrorFlashMessage message) = [hsx|<div class="alert alert-danger">{message}</div>|]-- styledInputClass _ FormField { fieldType = FileInput } = "form-control-file"- styledInputClass _ FormField {} = "form-control"- styledInputInvalidClass _ _ = "is-invalid"-- styledFormFieldHelp _ FormField { helpText = "" } = mempty- styledFormFieldHelp _ FormField { helpText } = [hsx|<small class="form-text text-muted">{helpText}</small>|]-- styledFormGroupClass = "form-group"-- styledValidationResultClass = "invalid-feedback"-- styledSubmitButtonClass = "btn btn-primary"--- styledFormField :: CSSFramework -> FormField -> Blaze.Html- styledFormField cssFramework@CSSFramework {styledValidationResult, styledTextFormField, styledCheckboxFormField, styledSelectFormField, styledRadioFormField, styledTextareaFormField} formField =- formGroup renderInner- where- renderInner = case formField.fieldType of- TextInput -> styledTextFormField cssFramework "text" formField validationResult- NumberInput -> styledTextFormField cssFramework "number" formField validationResult- PasswordInput -> styledTextFormField cssFramework "password" formField validationResult- ColorInput -> styledTextFormField cssFramework "color" formField validationResult- EmailInput -> styledTextFormField cssFramework "email" formField validationResult- DateInput -> styledTextFormField cssFramework "date" formField validationResult- DateTimeInput -> styledTextFormField cssFramework "datetime-local" formField validationResult- CheckboxInput -> styledCheckboxFormField cssFramework formField validationResult- HiddenInput -> styledTextFormField cssFramework "hidden" formField { disableLabel = True, disableGroup = True, disableValidationResult = True } validationResult- TextareaInput -> styledTextareaFormField cssFramework formField validationResult- SelectInput {} -> styledSelectFormField cssFramework formField validationResult- RadioInput {} -> styledRadioFormField cssFramework formField validationResult- FileInput -> styledTextFormField cssFramework "file" formField validationResult-- validationResult :: Blaze.Html- validationResult = unless formField.disableValidationResult (styledValidationResult cssFramework formField)-- -- | Wraps the input inside a @<div class="form-group">...</div>@ (unless @disableGroup = True@)- formGroup :: Blaze.Html -> Blaze.Html- formGroup renderInner = case formField of- FormField { disableGroup = True } -> renderInner- FormField { fieldInputId } -> styledFormGroup cssFramework fieldInputId renderInner--- styledFormGroup :: CSSFramework -> Text -> Blaze.Html -> Blaze.Html- styledFormGroup cssFramework@CSSFramework {styledFormGroupClass} fieldInputId renderInner =- [hsx|<div class={styledFormGroupClass} id={"form-group-" <> fieldInputId}>{renderInner}</div>|]-- styledCheckboxFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledCheckboxFormField cssFramework@CSSFramework {styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult = do- [hsx|<div class="form-check">{element}</div>|]- where- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- -- If the checkbox is checked off, the browser will not send the parameter as part of the form.- -- This will then make it impossible to set a field to False using a checkbox.- -- For that we add the "hidden" input type.- theInput = [hsx|- <input- type="checkbox"- name={fieldName}- class={classes ["form-check-input", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- id={fieldInputId}- checked={fieldValue == "yes"}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- />-- <input type="hidden" name={fieldName} value={inputValue False} />- |]--- element = if disableLabel- then [hsx|<div>- {theInput}- {validationResult}- {helpText}- </div>- |]- else [hsx|- {theInput}- <label- class={classes [("form-check-label", labelClass == ""), (labelClass, labelClass /= "")]}- for={fieldInputId}- >- {fieldLabel}- </label>-- {validationResult}- {helpText}- |]-- styledTextFormField :: CSSFramework -> Text -> FormField -> Blaze.Html -> Blaze.Html- styledTextFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} inputType formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =- [hsx|- {label}- <input- type={inputType}- name={fieldName}- placeholder={placeholder}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- value={maybeValue}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- />-- {validationResult}- {helpText}- |]- where- label = unless (disableLabel || null fieldLabel) [hsx|<label class={labelClass} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField- -- If there's no value, then we want to hide the "value" attribute.- maybeValue = if fieldValue == "" then Nothing else Just fieldValue-- styledSelectFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledSelectFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =- [hsx|- {label}- <select- name={fieldName}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- value={fieldValue}- disabled={disabled}- required={required}- autofocus={autofocus}- {...additionalAttributes}- >- <option selected={not isValueSelected} disabled={True}>{placeholder}</option>- {forEach (options fieldType) (getOption)}- </select>-- {validationResult}- {helpText}- |]- where- label = unless disableLabel [hsx|<label class={labelClass} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- isValueSelected = any (\(_, optionValue) -> optionValue == fieldValue) (options fieldType)-- -- Get a single option.- getOption (optionLabel, optionValue) = [hsx|- <option value={optionValue} selected={optionValue == fieldValue}>- {optionLabel}- </option>- |]-- styledTextareaFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledTextareaFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =- [hsx|- {label}- <textarea- name={fieldName}- placeholder={placeholder}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- >{fieldValue}</textarea>{validationResult}{helpText}|]- where- label = unless (disableLabel || null fieldLabel) [hsx|<label class={labelClass} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- styledValidationResult :: CSSFramework -> FormField -> Blaze.Html- styledValidationResult cssFramework formField@FormField { validatorResult = Just violation } =- let- className :: Text = cssFramework.styledValidationResultClass- message = case violation of- TextViolation text -> [hsx|{text}|]- HtmlViolation html -> Blaze.preEscapedToHtml html- in- [hsx|<div class={className}>{message}</div>|]- styledValidationResult _ _ = mempty-- styledSubmitButton cssFramework SubmitButton { label, buttonClass, buttonDisabled } =- let className :: Text = cssFramework.styledSubmitButtonClass- in [hsx|<button class={classes [(className, True), (buttonClass, not (null buttonClass))]} disabled={buttonDisabled} type="submit">{label}</button>|]-- styledPagination :: CSSFramework -> PaginationView -> Blaze.Html- styledPagination _ paginationView =- [hsx|-- <div class="d-flex justify-content-md-center">- <nav aria-label="Page Navigator" class="mr-2">- <ul class="pagination">- {paginationView.linkPrevious}- {paginationView.pageDotDotItems}- {paginationView.linkNext}- </ul>- </nav>-- <div class="form-row">- <div class="col-auto mr-2">- <select class="custom-select" id="maxItemsSelect" onchange="window.location.href = this.options[this.selectedIndex].dataset.url">- {paginationView.itemsPerPageSelector}- </select>- </div>- </div>-- </div>- |]-- styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html- styledPaginationPageLink _ pagination@Pagination {currentPage} pageUrl pageNumber =- let- linkClass = classes ["page-item", ("active", pageNumber == currentPage)]- in- [hsx|<li class={linkClass}><a class="page-link" href={pageUrl}>{show pageNumber}</a></li>|]--- styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html- styledPaginationDotDot _ _ =- [hsx|<li class="page-item"><a class="page-link">…</a></li>|]-- styledPaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html- styledPaginationItemsPerPageSelector _ pagination@Pagination {pageSize} itemsPerPageUrl =- let- oneOption :: Int -> Blaze.Html- oneOption n = [hsx|<option value={show n} selected={n == pageSize} data-url={itemsPerPageUrl n}>{n} items per page</option>|]- in- [hsx|{forEach [10,20,50,100,200] oneOption}|]-- styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html- styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =- let- prevClass = classes ["page-item", ("disabled", not $ hasPreviousPage pagination)]- url = if hasPreviousPage pagination then pageUrl else "#"- in- [hsx|- <li class={prevClass}>- <a class="page-link" href={url} aria-label="Previous">- <span aria-hidden="true">«</span>- <span class="sr-only">Previous</span>- </a>- </li>- |]-- styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html- styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =- let- nextClass = classes ["page-item", ("disabled", not $ hasNextPage pagination)]- url = if hasNextPage pagination then pageUrl else "#"- in- [hsx|- <li class={nextClass}>- <a class="page-link" href={url} aria-label="Next">- <span aria-hidden="true">»</span>- <span class="sr-only">Next</span>- </a>- </li>- |]-- styledBreadcrumb :: CSSFramework -> [BreadcrumbItem]-> BreadcrumbsView -> Blaze.Html- styledBreadcrumb _ _ breadcrumbsView = [hsx|- <nav>- <ol class="breadcrumb">- {breadcrumbsView.breadcrumbItems}-- </ol>- </nav>- |]--- styledBreadcrumbItem :: CSSFramework -> [ BreadcrumbItem ]-> BreadcrumbItem -> Bool -> Blaze.Html- styledBreadcrumbItem _ breadcrumbItems breadcrumbItem@BreadcrumbItem {breadcrumbLabel, url} isLast =- let- breadcrumbsClasses = classes ["breadcrumb-item", ("active", isLast)]- in- case url of- Nothing -> [hsx|<li class={breadcrumbsClasses}>{breadcrumbLabel}</li>|]- Just url -> [hsx|<li class={breadcrumbsClasses}><a href={url}>{breadcrumbLabel}</a></li>|]-----tailwind :: CSSFramework-tailwind = def- { styledFlashMessage- , styledTextFormField- , styledTextareaFormField- , styledCheckboxFormField- , styledSelectFormField- , styledRadioFormField- , styledSubmitButtonClass- , styledFormGroupClass- , styledFormFieldHelp- , styledInputClass- , styledInputInvalidClass- , styledValidationResultClass- , styledPagination- , styledPaginationLinkPrevious- , styledPaginationLinkNext- , styledPaginationPageLink- , styledPaginationDotDot- , styledPaginationItemsPerPageSelector- , styledBreadcrumb- , styledBreadcrumbItem- }- where- styledFlashMessage _ (SuccessFlashMessage message) = [hsx|<div class="bg-green-100 border border-green-500 text-green-900 px-4 py-3 rounded relative">{message}</div>|]- styledFlashMessage _ (ErrorFlashMessage message) = [hsx|<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{message}</div>|]-- styledCheckboxFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledCheckboxFormField cssFramework@CSSFramework {styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult = do- [hsx|<div class="form-check">{element}</div>|]- where- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- theInput = [hsx|- <div>- <input- type="checkbox"- name={fieldName}- class={classes ["form-check-input", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- id={fieldInputId}- checked={fieldValue == "yes"}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- />-- <input type="hidden" name={fieldName} value={inputValue False} />- </div>- |]--- element = if disableLabel- then [hsx|<div class="flex flex-row space-x-2">- {theInput}- <div class="flex flex-col space-y-2">- {validationResult}- {helpText}- </div>- </div>- |]- else [hsx|- <div class="flex flex-row space-x-2">- {theInput}- <div class="flex flex-col">- <label- class={classes ["font-medium text-gray-700", ("form-check-label", labelClass == ""), (labelClass, labelClass /= "")]}- for={fieldInputId}- >- {fieldLabel}- </label>-- {validationResult}- {helpText}- </div>- </div>- |]-- styledTextFormField :: CSSFramework -> Text -> FormField -> Blaze.Html -> Blaze.Html- styledTextFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} inputType formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =- [hsx|- {label}- <input- type={inputType}- name={fieldName}- placeholder={placeholder}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- value={maybeValue}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- />-- {validationResult}- {helpText}- |]- where- twLabelClass = "font-medium text-gray-700" <> " " <> labelClass- label = unless (disableLabel || null fieldLabel) [hsx|<label class={twLabelClass} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField- -- If there's no value, then we want to hide the "value" attribute.- maybeValue = if fieldValue == "" then Nothing else Just fieldValue--- styledTextareaFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledTextareaFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =- [hsx|- {label}- <textarea- name={fieldName}- placeholder={placeholder}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- required={required}- disabled={disabled}- autofocus={autofocus}- {...additionalAttributes}- >{fieldValue}</textarea>{validationResult}{helpText}- |]- where- twLabelClass = classes ["font-medium text-gray-700", (labelClass, not (null labelClass))]- label = unless (disableLabel || null fieldLabel) [hsx|<label class={twLabelClass} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField--- styledSelectFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledSelectFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =- [hsx|- {label}- <select- name={fieldName}- id={fieldInputId}- class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- value={fieldValue}- disabled={disabled}- required={required}- autofocus={autofocus}- {...additionalAttributes}- >- <option selected={not isValueSelected} disabled={True}>{placeholder}</option>- {forEach (options fieldType) (getOption)}- </select>-- {validationResult}- {helpText}- |]- where- twLabelClass = "font-medium text-gray-700" <> " " <> labelClass- label = unless disableLabel [hsx|<label class={twLabelClass} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- isValueSelected = any (\(_, optionValue) -> optionValue == fieldValue) (options fieldType)-- -- Get a single option.- getOption (optionLabel, optionValue) = [hsx|- <option value={optionValue} selected={optionValue == fieldValue}>- {optionLabel}- </option>- |]-- styledRadioFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html- styledRadioFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =- [hsx|- {label}- <fieldset- class={classes ["flex flex-col gap-2", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- autofocus={autofocus}- {...additionalAttributes}- >-- {forEach (options fieldType) (getRadio)}- </fieldset>-- {validationResult}- {helpText}- |]- where- label = unless disableLabel [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]- inputClass = (styledInputClass cssFramework formField, True)- inputInvalidClass = styledInputInvalidClass cssFramework formField- helpText = styledFormFieldHelp cssFramework formField-- -- Get a single radio button.- getRadio (optionLabel, optionValue) = [hsx|- <div class="flex flex-row gap-2 items-center">- <input- class={classes [(inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}- type="radio"- id={optionId}- name={fieldName}- value={optionValue}- checked={optionValue == fieldValue}- disabled={disabled}- required={required}- autofocus={autofocus}- {...additionalAttributes}- />- {label}- </div>- |]- where- optionId = fieldInputId <> "_" <> optionValue- label = unless disableLabel [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={optionId}>{optionLabel}</label>|]--- styledInputClass _ FormField {} = "focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md"- styledInputInvalidClass _ _ = "is-invalid"-- styledSubmitButtonClass = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"-- styledFormFieldHelp _ FormField { helpText = "" } = mempty- styledFormFieldHelp _ FormField { helpText } = [hsx|<p class="text-gray-600 text-xs italic">{helpText}</p>|]-- styledFormGroupClass = "flex flex-col my-6 space-y-2"-- styledValidationResultClass = "text-red-500 text-xs italic"-- styledPagination :: CSSFramework -> PaginationView -> Blaze.Html- styledPagination _ paginationView@PaginationView {pageUrl, pagination} =- let- currentPage = pagination.currentPage-- previousPageUrl = if hasPreviousPage pagination then pageUrl $ currentPage - 1 else "#"- nextPageUrl = if hasNextPage pagination then pageUrl $ currentPage + 1 else "#"-- defaultClass = "relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"- previousClass = classes- [ defaultClass- , ("disabled", not $ hasPreviousPage pagination)- ]- nextClass = classes- [ defaultClass- , ("disabled", not $ hasNextPage pagination)- ]-- previousMobileOnly =- [hsx|- <a href={previousPageUrl} class={previousClass}>- Previous- </a>- |]-- nextMobileOnly =- [hsx|- <a href={nextPageUrl} class={nextClass}>- Next- </a>- |]-- in- [hsx|- <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">- <div class="flex-1 flex justify-between sm:hidden">- {previousMobileOnly}- {nextMobileOnly}- </div>- <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">- <div class="text-sm text-gray-700">- <select class="px-4 py-3" id="maxItemsSelect" onchange="window.location.href = this.options[this.selectedIndex].dataset.url">- {paginationView.itemsPerPageSelector}- </select>- </div>- <div>- <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">- {paginationView.linkPrevious}-- {paginationView.pageDotDotItems}-- {paginationView.linkNext}- </nav>- </div>- </div>- </div>- |]-- styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html- styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =- let- prevClass = classes- [ "relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"- , ("disabled", not $ hasPreviousPage pagination)- ]-- url = if hasPreviousPage pagination then pageUrl else "#"-- in- [hsx|- <a href={url} class={prevClass}>- <span class="sr-only">Previous</span>- <!-- Heroicon name: solid/chevron-left -->- <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">- <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />- </svg>- </a>- |]-- styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html- styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =- let- nextClass = classes- [ "relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"- , ("disabled", not $ hasNextPage pagination)- ]-- url = if hasNextPage pagination then pageUrl else "#"- in- [hsx|- <a href={url} class={nextClass}>- <span class="sr-only">Next</span>- <!-- Heroicon name: solid/chevron-right -->- <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">- <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />- </svg>- </a>-- |]-- styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html- styledPaginationPageLink _ pagination@Pagination {currentPage} pageUrl pageNumber =- let- linkClass = classes- [ "relative inline-flex items-center px-4 py-2 border text-sm font-medium"- -- Current page- , ("z-10 bg-blue-50 border-blue-500 text-blue-600", pageNumber == currentPage)- -- Not current page- , ("bg-white border-gray-300 text-gray-500 hover:bg-gray-50", pageNumber /= currentPage)- ]- in- [hsx|- <a href={pageUrl} aria-current={pageNumber == currentPage} class={linkClass}>- {show pageNumber}- </a>- |]--- styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html- styledPaginationDotDot _ _ =- [hsx|- <span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">- ...- </span>- |]--- styledPaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html- styledPaginationItemsPerPageSelector _ pagination@Pagination {pageSize} itemsPerPageUrl =- let- oneOption :: Int -> Blaze.Html- oneOption n = [hsx|<option value={show n} selected={n == pageSize} data-url={itemsPerPageUrl n}>{n} items per page</option>|]- in- [hsx|{forEach [10,20,50,100,200] oneOption}|]--- styledBreadcrumb :: CSSFramework -> [BreadcrumbItem]-> BreadcrumbsView -> Blaze.Html- styledBreadcrumb _ _ breadcrumbsView = [hsx|- <nav class="breadcrumbs bg-white my-4" aria-label="Breadcrumb">- <ol class="flex items-center space-x-2" role="list">- {breadcrumbsView.breadcrumbItems}- </ol>- </nav>- |]--- styledBreadcrumbItem :: CSSFramework -> [ BreadcrumbItem ]-> BreadcrumbItem -> Bool -> Blaze.Html- styledBreadcrumbItem _ breadcrumbItems breadcrumbItem@BreadcrumbItem {breadcrumbLabel, url} isLast =- let- breadcrumbsClasses = classes ["flex flex-row space-x-2 text-gray-600 items-center", ("active", isLast)]-- -- Show chevron if item isn't the active one (i.e. the last one).- chevronRight = unless isLast [hsx|- <!-- heroicons.com chevron-right -->- <svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />- </svg>- |]- in- case url of- Nothing -> [hsx|- <li class={breadcrumbsClasses}>- {breadcrumbLabel}- {chevronRight}- </li>- |]- Just url -> [hsx|- <li class={breadcrumbsClasses}>- <a class="hover:text-gray-700" href={url}>{breadcrumbLabel}</a>- {chevronRight}- </li>- |]+module IHP.View.CSSFramework+ ( module IHP.View.CSSFramework.Bootstrap+ , module IHP.View.CSSFramework.Tailwind+ , module IHP.View.CSSFramework.Unstyled+ ) where++import IHP.View.CSSFramework.Unstyled+import IHP.View.CSSFramework.Bootstrap+import IHP.View.CSSFramework.Tailwind
@@ -0,0 +1,307 @@+{-|+Module: IHP.View.CSSFramework.Bootstrap+Description: Bootstrap CSS framework implementations+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.View.CSSFramework.Bootstrap (bootstrap, bootstrap4, bootstrapBase) where++import Prelude hiding (null)+import Data.Text (null)+import Data.ByteString (ByteString)+import Data.Maybe (isJust)+import Control.Monad (unless)+import IHP.HaskellSupport (forEach)+import IHP.InputValue (inputValue)+import qualified Text.Blaze.Html5 as Blaze+import IHP.HSX.QQ (hsx)+import IHP.HSX.ToHtml ()+import IHP.View.Types+import IHP.View.Classes+import IHP.Breadcrumb.Types+import IHP.Pagination.Helpers+import IHP.Pagination.Types+import Network.Wai.Middleware.FlashMessages (FlashMessage (..))+import IHP.View.CSSFramework.Unstyled (unstyled)++-- | Shared Bootstrap rendering used by both 'bootstrap' and 'bootstrap4'+bootstrapBase :: CSSFramework+bootstrapBase = unstyled+ { styledFlashMessage+ , styledInputClass+ , styledInputInvalidClass+ , styledValidationResultClass+ , styledSubmitButtonClass+ , styledCheckboxFormField+ , styledRadioFormField+ , styledPaginationPageLink+ , styledPaginationDotDot+ , styledPaginationItemsPerPageSelector+ , styledBreadcrumb+ , styledBreadcrumbItem+ }+ where+ styledFlashMessage _ (SuccessFlashMessage message) = [hsx|<div class="alert alert-success">{message}</div>|]+ styledFlashMessage _ (ErrorFlashMessage message) = [hsx|<div class="alert alert-danger">{message}</div>|]++ styledInputClass _ FormField { fieldType = FileInput } = "form-control-file"+ styledInputClass _ FormField {} = "form-control"+ styledInputInvalidClass _ _ = "is-invalid"++ styledValidationResultClass = "invalid-feedback"+ styledSubmitButtonClass = "btn btn-primary"++ styledCheckboxFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+ styledCheckboxFormField cssFramework@CSSFramework {styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult = do+ [hsx|<div class="form-check">{element}</div>|]+ where+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++ theInput = [hsx|+ <input+ type="checkbox"+ name={fieldName}+ class={classes ["form-check-input", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ id={fieldInputId}+ checked={fieldValue == "yes"}+ required={required}+ disabled={disabled}+ autofocus={autofocus}+ {...additionalAttributes}+ />++ <input type="hidden" name={fieldName} value={inputValue False} />+ |]++ element = if disableLabel+ then [hsx|<div>+ {theInput}+ {validationResult}+ {helpText}+ </div>+ |]+ else [hsx|+ {theInput}+ <label+ class={classes [("form-check-label", labelClass == ""), (labelClass, labelClass /= "")]}+ for={fieldInputId}+ >+ {fieldLabel}+ </label>++ {validationResult}+ {helpText}+ |]++ styledRadioFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+ styledRadioFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =+ [hsx|+ {label}+ <fieldset>+ {forEach (options fieldType) (getRadio)}+ </fieldset>++ {validationResult}+ {helpText}+ |]+ where+ label = unless disableLabel [hsx|<label class={classes [(cssFramework.styledLabelClass, True), (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++ getRadio (optionLabel, optionValue) = [hsx|+ <div class="form-check">+ <input+ class={classes ["form-check-input", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ type="radio"+ id={optionId}+ name={fieldName}+ value={optionValue}+ checked={optionValue == fieldValue}+ disabled={disabled}+ required={required}+ autofocus={autofocus}+ {...additionalAttributes}+ />+ {radioLabel}+ </div>+ |]+ where+ optionId = fieldInputId <> "_" <> optionValue+ radioLabel = unless disableLabel [hsx|<label class={classes ["form-check-label", (labelClass, labelClass /= "")]} for={optionId}>{optionLabel}</label>|]++ styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html+ styledPaginationPageLink _ pagination@Pagination {currentPage} pageUrl pageNumber =+ let+ linkClass = classes ["page-item", ("active", pageNumber == currentPage)]+ in+ [hsx|<li class={linkClass}><a class="page-link" href={pageUrl}>{show pageNumber}</a></li>|]++ styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html+ styledPaginationDotDot _ _ =+ [hsx|<li class="page-item"><a class="page-link">…</a></li>|]++ styledPaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html+ styledPaginationItemsPerPageSelector _ pagination@Pagination {pageSize} itemsPerPageUrl =+ let+ oneOption :: Int -> Blaze.Html+ oneOption n = [hsx|<option value={show n} selected={n == pageSize} data-url={itemsPerPageUrl n}>{n} items per page</option>|]+ in+ [hsx|{forEach [10,20,50,100,200] oneOption}|]++ styledBreadcrumb :: CSSFramework -> [BreadcrumbItem]-> BreadcrumbsView -> Blaze.Html+ styledBreadcrumb _ _ breadcrumbsView = [hsx|+ <nav>+ <ol class="breadcrumb">+ {breadcrumbsView.breadcrumbItems}++ </ol>+ </nav>+ |]++ styledBreadcrumbItem :: CSSFramework -> [ BreadcrumbItem ]-> BreadcrumbItem -> Bool -> Blaze.Html+ styledBreadcrumbItem _ breadcrumbItems breadcrumbItem@BreadcrumbItem {breadcrumbLabel, url} isLast =+ let+ breadcrumbsClasses = classes ["breadcrumb-item", ("active", isLast)]+ in+ case url of+ Nothing -> [hsx|<li class={breadcrumbsClasses}>{breadcrumbLabel}</li>|]+ Just url -> [hsx|<li class={breadcrumbsClasses}><a href={url}>{breadcrumbLabel}</a></li>|]++bootstrap :: CSSFramework+bootstrap = bootstrapBase+ { styledLabelClass+ , styledFormGroupClass+ , styledFormFieldHelp+ , styledPagination+ , styledPaginationLinkPrevious+ , styledPaginationLinkNext+ }+ where+ styledLabelClass = "form-label"+ styledFormGroupClass = "mb-3"++ styledFormFieldHelp _ FormField { helpText = "" } = mempty+ styledFormFieldHelp _ FormField { helpText } = [hsx|<small class="form-text">{helpText}</small>|]++ styledPagination :: CSSFramework -> PaginationView -> Blaze.Html+ styledPagination _ paginationView =+ [hsx|++ <div class="d-flex justify-content-md-center">+ <nav aria-label="Page Navigator" class="me-2">+ <ul class="pagination">+ {paginationView.linkPrevious}+ {paginationView.pageDotDotItems}+ {paginationView.linkNext}+ </ul>+ </nav>++ <div class="row">+ <div class="col-auto me-2">+ <select class="form-select" id="maxItemsSelect" onchange="window.location.href = this.options[this.selectedIndex].dataset.url">+ {paginationView.itemsPerPageSelector}+ </select>+ </div>+ </div>++ </div>+ |]++ styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+ styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =+ let+ prevClass = classes ["page-item", ("disabled", not $ hasPreviousPage pagination)]+ url = if hasPreviousPage pagination then pageUrl else "#"+ in+ [hsx|+ <li class={prevClass}>+ <a class="page-link" href={url} aria-label="Previous">+ <span aria-hidden="true">«</span>+ <span class="visually-hidden">Previous</span>+ </a>+ </li>+ |]++ styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+ styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =+ let+ nextClass = classes ["page-item", ("disabled", not $ hasNextPage pagination)]+ url = if hasNextPage pagination then pageUrl else "#"+ in+ [hsx|+ <li class={nextClass}>+ <a class="page-link" href={url} aria-label="Next">+ <span aria-hidden="true">»</span>+ <span class="visually-hidden">Next</span>+ </a>+ </li>+ |]++bootstrap4 :: CSSFramework+bootstrap4 = bootstrapBase+ { styledFormGroupClass+ , styledFormFieldHelp+ , styledPagination+ , styledPaginationLinkPrevious+ , styledPaginationLinkNext+ }+ where+ styledFormGroupClass = "form-group"++ styledFormFieldHelp _ FormField { helpText = "" } = mempty+ styledFormFieldHelp _ FormField { helpText } = [hsx|<small class="form-text text-muted">{helpText}</small>|]++ styledPagination :: CSSFramework -> PaginationView -> Blaze.Html+ styledPagination _ paginationView =+ [hsx|++ <div class="d-flex justify-content-md-center">+ <nav aria-label="Page Navigator" class="mr-2">+ <ul class="pagination">+ {paginationView.linkPrevious}+ {paginationView.pageDotDotItems}+ {paginationView.linkNext}+ </ul>+ </nav>++ <div class="form-row">+ <div class="col-auto mr-2">+ <select class="custom-select" id="maxItemsSelect" onchange="window.location.href = this.options[this.selectedIndex].dataset.url">+ {paginationView.itemsPerPageSelector}+ </select>+ </div>+ </div>++ </div>+ |]++ styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+ styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =+ let+ prevClass = classes ["page-item", ("disabled", not $ hasPreviousPage pagination)]+ url = if hasPreviousPage pagination then pageUrl else "#"+ in+ [hsx|+ <li class={prevClass}>+ <a class="page-link" href={url} aria-label="Previous">+ <span aria-hidden="true">«</span>+ <span class="sr-only">Previous</span>+ </a>+ </li>+ |]++ styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+ styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =+ let+ nextClass = classes ["page-item", ("disabled", not $ hasNextPage pagination)]+ url = if hasNextPage pagination then pageUrl else "#"+ in+ [hsx|+ <li class={nextClass}>+ <a class="page-link" href={url} aria-label="Next">+ <span aria-hidden="true">»</span>+ <span class="sr-only">Next</span>+ </a>+ </li>+ |]
@@ -0,0 +1,333 @@+{-|+Module: IHP.View.CSSFramework.Tailwind+Description: Tailwind CSS framework implementation+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.View.CSSFramework.Tailwind (tailwind) where++import Prelude hiding (null)+import Data.Text (null)+import Data.ByteString (ByteString)+import Data.Maybe (isJust)+import Control.Monad (unless)+import IHP.HaskellSupport (forEach)+import IHP.InputValue (inputValue)+import qualified Text.Blaze.Html5 as Blaze+import IHP.HSX.QQ (hsx)+import IHP.HSX.ToHtml ()+import IHP.View.Types+import IHP.View.Classes+import IHP.Breadcrumb.Types+import IHP.Pagination.Helpers+import IHP.Pagination.Types+import Network.Wai.Middleware.FlashMessages (FlashMessage (..))+import IHP.View.CSSFramework.Unstyled (unstyled)++tailwind :: CSSFramework+tailwind = unstyled+ { styledFlashMessage+ , styledLabelClass+ , styledCheckboxFormField+ , styledRadioFormField+ , styledSubmitButtonClass+ , styledFormGroupClass+ , styledFormFieldHelp+ , styledInputClass+ , styledInputInvalidClass+ , styledValidationResultClass+ , styledPagination+ , styledPaginationLinkPrevious+ , styledPaginationLinkNext+ , styledPaginationPageLink+ , styledPaginationDotDot+ , styledPaginationItemsPerPageSelector+ , styledBreadcrumb+ , styledBreadcrumbItem+ }+ where+ styledFlashMessage _ (SuccessFlashMessage message) = [hsx|<div class="bg-green-100 border border-green-500 text-green-900 px-4 py-3 rounded relative">{message}</div>|]+ styledFlashMessage _ (ErrorFlashMessage message) = [hsx|<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{message}</div>|]++ styledLabelClass = "font-medium text-gray-700"++ styledCheckboxFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+ styledCheckboxFormField cssFramework@CSSFramework {styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult = do+ [hsx|<div class="form-check">{element}</div>|]+ where+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++ theInput = [hsx|+ <div>+ <input+ type="checkbox"+ name={fieldName}+ class={classes ["form-check-input", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ id={fieldInputId}+ checked={fieldValue == "yes"}+ required={required}+ disabled={disabled}+ autofocus={autofocus}+ {...additionalAttributes}+ />++ <input type="hidden" name={fieldName} value={inputValue False} />+ </div>+ |]+++ element = if disableLabel+ then [hsx|<div class="flex flex-row space-x-2">+ {theInput}+ <div class="flex flex-col space-y-2">+ {validationResult}+ {helpText}+ </div>+ </div>+ |]+ else [hsx|+ <div class="flex flex-row space-x-2">+ {theInput}+ <div class="flex flex-col">+ <label+ class={classes ["font-medium text-gray-700", ("form-check-label", labelClass == ""), (labelClass, labelClass /= "")]}+ for={fieldInputId}+ >+ {fieldLabel}+ </label>++ {validationResult}+ {helpText}+ </div>+ </div>+ |]++ styledRadioFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+ styledRadioFormField cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =+ [hsx|+ {label}+ <fieldset+ class={classes ["flex flex-col gap-2", (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ autofocus={autofocus}+ {...additionalAttributes}+ >++ {forEach (options fieldType) (getRadio)}+ </fieldset>++ {validationResult}+ {helpText}+ |]+ where+ label = unless disableLabel [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]+ inputClass = (styledInputClass cssFramework formField, True)+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++ -- Get a single radio button.+ getRadio (optionLabel, optionValue) = [hsx|+ <div class="flex flex-row gap-2 items-center">+ <input+ class={classes [(inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ type="radio"+ id={optionId}+ name={fieldName}+ value={optionValue}+ checked={optionValue == fieldValue}+ disabled={disabled}+ required={required}+ autofocus={autofocus}+ {...additionalAttributes}+ />+ {label}+ </div>+ |]+ where+ optionId = fieldInputId <> "_" <> optionValue+ label = unless disableLabel [hsx|<label class={classes ["form-label", (labelClass, labelClass /= "")]} for={optionId}>{optionLabel}</label>|]+++ styledInputClass _ FormField {} = "focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md"+ styledInputInvalidClass _ _ = "is-invalid"++ styledSubmitButtonClass = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"++ styledFormFieldHelp _ FormField { helpText = "" } = mempty+ styledFormFieldHelp _ FormField { helpText } = [hsx|<p class="text-gray-600 text-xs italic">{helpText}</p>|]++ styledFormGroupClass = "flex flex-col my-6 space-y-2"++ styledValidationResultClass = "text-red-500 text-xs italic"++ styledPagination :: CSSFramework -> PaginationView -> Blaze.Html+ styledPagination _ paginationView@PaginationView {pageUrl, pagination} =+ let+ currentPage = pagination.currentPage++ previousPageUrl = if hasPreviousPage pagination then pageUrl $ currentPage - 1 else "#"+ nextPageUrl = if hasNextPage pagination then pageUrl $ currentPage + 1 else "#"++ defaultClass = "relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"+ previousClass = classes+ [ defaultClass+ , ("disabled", not $ hasPreviousPage pagination)+ ]+ nextClass = classes+ [ defaultClass+ , ("disabled", not $ hasNextPage pagination)+ ]++ previousMobileOnly =+ [hsx|+ <a href={previousPageUrl} class={previousClass}>+ Previous+ </a>+ |]++ nextMobileOnly =+ [hsx|+ <a href={nextPageUrl} class={nextClass}>+ Next+ </a>+ |]++ in+ [hsx|+ <div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">+ <div class="flex-1 flex justify-between sm:hidden">+ {previousMobileOnly}+ {nextMobileOnly}+ </div>+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">+ <div class="text-sm text-gray-700">+ <select class="px-4 py-3" id="maxItemsSelect" onchange="window.location.href = this.options[this.selectedIndex].dataset.url">+ {paginationView.itemsPerPageSelector}+ </select>+ </div>+ <div>+ <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">+ {paginationView.linkPrevious}++ {paginationView.pageDotDotItems}++ {paginationView.linkNext}+ </nav>+ </div>+ </div>+ </div>+ |]++ styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+ styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =+ let+ prevClass = classes+ [ "relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"+ , ("disabled", not $ hasPreviousPage pagination)+ ]++ url = if hasPreviousPage pagination then pageUrl else "#"++ in+ [hsx|+ <a href={url} class={prevClass}>+ <span class="sr-only">Previous</span>+ <!-- Heroicon name: solid/chevron-left -->+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />+ </svg>+ </a>+ |]++ styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+ styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =+ let+ nextClass = classes+ [ "relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"+ , ("disabled", not $ hasNextPage pagination)+ ]++ url = if hasNextPage pagination then pageUrl else "#"+ in+ [hsx|+ <a href={url} class={nextClass}>+ <span class="sr-only">Next</span>+ <!-- Heroicon name: solid/chevron-right -->+ <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />+ </svg>+ </a>++ |]++ styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html+ styledPaginationPageLink _ pagination@Pagination {currentPage} pageUrl pageNumber =+ let+ linkClass = classes+ [ "relative inline-flex items-center px-4 py-2 border text-sm font-medium"+ -- Current page+ , ("z-10 bg-blue-50 border-blue-500 text-blue-600", pageNumber == currentPage)+ -- Not current page+ , ("bg-white border-gray-300 text-gray-500 hover:bg-gray-50", pageNumber /= currentPage)+ ]+ in+ [hsx|+ <a href={pageUrl} aria-current={pageNumber == currentPage} class={linkClass}>+ {show pageNumber}+ </a>+ |]+++ styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html+ styledPaginationDotDot _ _ =+ [hsx|+ <span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">+ ...+ </span>+ |]+++ styledPaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html+ styledPaginationItemsPerPageSelector _ pagination@Pagination {pageSize} itemsPerPageUrl =+ let+ oneOption :: Int -> Blaze.Html+ oneOption n = [hsx|<option value={show n} selected={n == pageSize} data-url={itemsPerPageUrl n}>{n} items per page</option>|]+ in+ [hsx|{forEach [10,20,50,100,200] oneOption}|]+++ styledBreadcrumb :: CSSFramework -> [BreadcrumbItem]-> BreadcrumbsView -> Blaze.Html+ styledBreadcrumb _ _ breadcrumbsView = [hsx|+ <nav class="breadcrumbs bg-white my-4" aria-label="Breadcrumb">+ <ol class="flex items-center space-x-2" role="list">+ {breadcrumbsView.breadcrumbItems}+ </ol>+ </nav>+ |]+++ styledBreadcrumbItem :: CSSFramework -> [ BreadcrumbItem ]-> BreadcrumbItem -> Bool -> Blaze.Html+ styledBreadcrumbItem _ breadcrumbItems breadcrumbItem@BreadcrumbItem {breadcrumbLabel, url} isLast =+ let+ breadcrumbsClasses = classes ["flex flex-row space-x-2 text-gray-600 items-center", ("active", isLast)]++ -- Show chevron if item isn't the active one (i.e. the last one).+ chevronRight = unless isLast [hsx|+ <!-- heroicons.com chevron-right -->+ <svg xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />+ </svg>+ |]+ in+ case url of+ Nothing -> [hsx|+ <li class={breadcrumbsClasses}>+ {breadcrumbLabel}+ {chevronRight}+ </li>+ |]+ Just url -> [hsx|+ <li class={breadcrumbsClasses}>+ <a class="hover:text-gray-700" href={url}>{breadcrumbLabel}</a>+ {chevronRight}+ </li>+ |]
@@ -0,0 +1,401 @@+{-|+Module: IHP.View.CSSFramework.Unstyled+Description: Unstyled CSSFramework (base implementation)+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.View.CSSFramework.Unstyled+( unstyled+, styledFlashMessageDefault+, styledFlashMessagesDefault+, styledFormFieldDefault+, styledFormGroupDefault+, styledCheckboxFormFieldDefault+, styledTextFormFieldDefault+, styledSelectFormFieldDefault+, styledRadioFormFieldDefault+, styledTextareaFormFieldDefault+, styledValidationResultDefault+, styledSubmitButtonDefault+, styledFormFieldHelpDefault+, styledInputClassDefault+, styledInputInvalidClassDefault+, styledPaginationDefault+, styledPaginationPageLinkDefault+, styledPaginationDotDotDefault+, styledPaginationItemsPerPageSelectorDefault+, styledPaginationLinkPreviousDefault+, styledPaginationLinkNextDefault+, styledBreadcrumbDefault+, styledBreadcrumbItemDefault+) where++import Prelude hiding (null)+import Data.Text (Text, null)+import Data.ByteString (ByteString)+import Data.Maybe (isJust)+import Control.Monad (unless)+import IHP.HaskellSupport (forEach)+import IHP.ModelSupport.Types (Violation(..))+import IHP.InputValue (inputValue)+import Network.Wai.Middleware.FlashMessages (FlashMessage (..))+import qualified Text.Blaze.Html5 as Blaze+import IHP.HSX.QQ (hsx)+import IHP.HSX.ToHtml ()+import IHP.View.Types+import IHP.View.Classes+import IHP.Breadcrumb.Types+import IHP.Pagination.Helpers+import IHP.Pagination.Types++-- | Provides a truly unstyled CSSFramework with no CSS classes+--+-- This way we can later add more properties to the CSSFramework without having to update all the CSS Frameworks manually+unstyled :: CSSFramework+unstyled = CSSFramework+ { styledFlashMessage = styledFlashMessageDefault+ , styledFlashMessages = styledFlashMessagesDefault+ , styledFormField = styledFormFieldDefault+ , styledTextFormField = styledTextFormFieldDefault+ , styledTextareaFormField = styledTextareaFormFieldDefault+ , styledCheckboxFormField = styledCheckboxFormFieldDefault+ , styledSelectFormField = styledSelectFormFieldDefault+ , styledRadioFormField = styledRadioFormFieldDefault+ , styledFormGroup = styledFormGroupDefault+ , styledSubmitButton = styledSubmitButtonDefault+ , styledSubmitButtonClass = ""+ , styledFormFieldHelp = styledFormFieldHelpDefault+ , styledInputClass = styledInputClassDefault+ , styledInputInvalidClass = styledInputInvalidClassDefault+ , styledFormGroupClass = ""+ , styledLabelClass = ""+ , styledValidationResult = styledValidationResultDefault+ , styledValidationResultClass = ""+ , styledPagination = styledPaginationDefault+ , styledPaginationPageLink = styledPaginationPageLinkDefault+ , styledPaginationDotDot = styledPaginationDotDotDefault+ , styledPaginationItemsPerPageSelector = styledPaginationItemsPerPageSelectorDefault+ , styledPaginationLinkPrevious = styledPaginationLinkPreviousDefault+ , styledPaginationLinkNext = styledPaginationLinkNextDefault+ , styledBreadcrumb = styledBreadcrumbDefault+ , styledBreadcrumbItem = styledBreadcrumbItemDefault+ }++styledFlashMessageDefault :: CSSFramework -> FlashMessage -> Blaze.Html+styledFlashMessageDefault cssFramework = \case+ SuccessFlashMessage message -> [hsx|<div>{message}</div>|]+ ErrorFlashMessage message -> [hsx|<div>{message}</div>|]++styledFlashMessagesDefault :: CSSFramework -> [FlashMessage] -> Blaze.Html+styledFlashMessagesDefault cssFramework flashMessages = forEach flashMessages (cssFramework.styledFlashMessage cssFramework)++styledFormFieldDefault :: CSSFramework -> FormField -> Blaze.Html+styledFormFieldDefault cssFramework@CSSFramework {styledValidationResult, styledTextFormField, styledCheckboxFormField, styledSelectFormField, styledRadioFormField, styledTextareaFormField, styledFormGroup} formField0 =+ formGroup renderInner+ where+ -- Hidden inputs don't need labels, groups, or validation display+ formField = case formField0.fieldType of+ HiddenInput -> formField0 { disableLabel = True, disableGroup = True, disableValidationResult = True }+ _ -> formField0++ renderInner = case formField.fieldType of+ TextInput -> styledTextFormField cssFramework "text" formField validationResult+ NumberInput -> styledTextFormField cssFramework "number" formField validationResult+ UrlInput -> styledTextFormField cssFramework "url" formField validationResult+ PasswordInput -> styledTextFormField cssFramework "password" formField validationResult+ ColorInput -> styledTextFormField cssFramework "color" formField validationResult+ EmailInput -> styledTextFormField cssFramework "email" formField validationResult+ DateInput -> styledTextFormField cssFramework "date" formField validationResult+ DateTimeInput -> styledTextFormField cssFramework "datetime-local" formField validationResult+ CheckboxInput -> styledCheckboxFormField cssFramework formField validationResult+ HiddenInput -> styledTextFormField cssFramework "hidden" formField validationResult+ TextareaInput -> styledTextareaFormField cssFramework formField validationResult+ SelectInput {} -> styledSelectFormField cssFramework formField validationResult+ RadioInput {} -> styledRadioFormField cssFramework formField validationResult+ FileInput -> styledTextFormField cssFramework "file" formField validationResult++ validationResult :: Blaze.Html+ validationResult = unless formField.disableValidationResult (styledValidationResult cssFramework formField)++ -- | Wraps the input inside a @<div class="form-group">...</div>@ (unless @disableGroup = True@)+ formGroup :: Blaze.Html -> Blaze.Html+ formGroup renderInner = case formField of+ FormField { disableGroup = True } -> renderInner+ FormField { fieldInputId } -> styledFormGroup cssFramework fieldInputId renderInner++styledFormGroupDefault :: CSSFramework -> Text -> Blaze.Html -> Blaze.Html+styledFormGroupDefault cssFramework@CSSFramework {styledFormGroupClass} fieldInputId renderInner =+ [hsx|<div class={styledFormGroupClass} id={"form-group-" <> fieldInputId}>{renderInner}</div>|]++styledCheckboxFormFieldDefault :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+styledCheckboxFormFieldDefault cssFramework@CSSFramework {styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult = do+ [hsx|<div>{element}</div>|]+ where+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++ theInput = [hsx|+ <input+ type="checkbox"+ name={fieldName}+ class={classes [(inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ id={fieldInputId}+ checked={fieldValue == "yes"}+ required={required}+ disabled={disabled}+ autofocus={autofocus}+ {...additionalAttributes}+ />++ <input type="hidden" name={fieldName} value={inputValue False} />+ |]++ element = if disableLabel+ then [hsx|<div>+ {theInput}+ {validationResult}+ {helpText}+ </div>+ |]+ else [hsx|+ {theInput}+ <label+ class={classes [(labelClass, labelClass /= "")]}+ for={fieldInputId}+ >+ {fieldLabel}+ </label>++ {validationResult}+ {helpText}+ |]++styledTextFormFieldDefault :: CSSFramework -> Text -> FormField -> Blaze.Html -> Blaze.Html+styledTextFormFieldDefault cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} inputType formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =+ [hsx|+ {label}+ <input+ type={inputType}+ name={fieldName}+ placeholder={placeholder}+ id={fieldInputId}+ class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ value={maybeValue}+ required={required}+ disabled={disabled}+ autofocus={autofocus}+ {...additionalAttributes}+ />++ {validationResult}+ {helpText}+ |]+ where+ label = unless (disableLabel || null fieldLabel) [hsx|<label class={classes [(cssFramework.styledLabelClass, True), (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]+ inputClass = (styledInputClass cssFramework formField, True)+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField+ maybeValue = if fieldValue == "" && inputType /= "date" && inputType /= "datetime-local"+ then Nothing+ else Just fieldValue++styledSelectFormFieldDefault :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+styledSelectFormFieldDefault cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =+ [hsx|+ {label}+ <select+ name={fieldName}+ id={fieldInputId}+ class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ value={fieldValue}+ disabled={disabled}+ required={required}+ autofocus={autofocus}+ {...additionalAttributes}+ >+ <option value="" selected={not isValueSelected} disabled={True}>{placeholder}</option>+ {forEach (options fieldType) (getOption)}+ </select>++ {validationResult}+ {helpText}+ |]+ where+ label = unless disableLabel [hsx|<label class={classes [(cssFramework.styledLabelClass, True), (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]+ inputClass = (styledInputClass cssFramework formField, True)+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++ isValueSelected = any (\(_, optionValue) -> optionValue == fieldValue) (options fieldType)++ getOption (optionLabel, optionValue) = [hsx|+ <option value={optionValue} selected={optionValue == fieldValue}>+ {optionLabel}+ </option>+ |]++styledRadioFormFieldDefault :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+styledRadioFormFieldDefault cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, placeholder, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, required, autofocus } validationResult =+ [hsx|+ {label}+ <fieldset>+ {forEach (options fieldType) (getRadio)}+ </fieldset>++ {validationResult}+ {helpText}+ |]+ where+ label = unless disableLabel [hsx|<label class={classes [(cssFramework.styledLabelClass, True), (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++ getRadio (optionLabel, optionValue) = [hsx|+ <div>+ <input+ class={classes [(inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ type="radio"+ id={optionId}+ name={fieldName}+ value={optionValue}+ checked={optionValue == fieldValue}+ disabled={disabled}+ required={required}+ autofocus={autofocus}+ {...additionalAttributes}+ />+ {radioLabel}+ </div>+ |]+ where+ optionId = fieldInputId <> "_" <> optionValue+ radioLabel = unless disableLabel [hsx|<label class={classes [(labelClass, labelClass /= "")]} for={optionId}>{optionLabel}</label>|]++styledTextareaFormFieldDefault :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html+styledTextareaFormFieldDefault cssFramework@CSSFramework {styledInputClass, styledInputInvalidClass, styledFormFieldHelp} formField@FormField {fieldType, fieldName, fieldLabel, fieldValue, fieldInputId, validatorResult, fieldClass, disabled, disableLabel, disableValidationResult, additionalAttributes, labelClass, placeholder, required, autofocus } validationResult =+ [hsx|+ {label}+ <textarea+ name={fieldName}+ placeholder={placeholder}+ id={fieldInputId}+ class={classes [inputClass, (inputInvalidClass, isJust validatorResult), (fieldClass, not (null fieldClass))]}+ required={required}+ disabled={disabled}+ autofocus={autofocus}+ {...additionalAttributes}+ >{fieldValue}</textarea>{validationResult}{helpText}|]+ where+ label = unless (disableLabel || null fieldLabel) [hsx|<label class={classes [(cssFramework.styledLabelClass, True), (labelClass, labelClass /= "")]} for={fieldInputId}>{fieldLabel}</label>|]+ inputClass = (styledInputClass cssFramework formField, True)+ inputInvalidClass = styledInputInvalidClass cssFramework formField+ helpText = styledFormFieldHelp cssFramework formField++styledValidationResultDefault :: CSSFramework -> FormField -> Blaze.Html+styledValidationResultDefault cssFramework formField@FormField { validatorResult = Just violation } =+ let+ className :: Text = cssFramework.styledValidationResultClass+ message = case violation of+ TextViolation text -> [hsx|{text}|]+ HtmlViolation html -> Blaze.preEscapedToHtml html+ in+ [hsx|<div class={className}>{message}</div>|]+styledValidationResultDefault _ _ = mempty++styledSubmitButtonDefault :: CSSFramework -> SubmitButton -> Blaze.Html+styledSubmitButtonDefault cssFramework SubmitButton { label, buttonClass, buttonDisabled } =+ let className :: Text = cssFramework.styledSubmitButtonClass+ in [hsx|<button class={classes [(className, True), (buttonClass, not (null buttonClass))]} disabled={buttonDisabled} type="submit">{label}</button>|]++styledFormFieldHelpDefault :: CSSFramework -> FormField -> Blaze.Html+styledFormFieldHelpDefault _ FormField { helpText = "" } = mempty+styledFormFieldHelpDefault _ FormField { helpText } = [hsx|<p>{helpText}</p>|]++styledInputClassDefault :: CSSFramework -> FormField -> Text+styledInputClassDefault _ _ = ""++styledInputInvalidClassDefault :: CSSFramework -> FormField -> Text+styledInputInvalidClassDefault _ _ = "invalid"++styledPaginationDefault :: CSSFramework -> PaginationView -> Blaze.Html+styledPaginationDefault _ paginationView =+ [hsx|++ <nav aria-label="Page Navigator">+ <ul>+ {paginationView.linkPrevious}+ {paginationView.pageDotDotItems}+ {paginationView.linkNext}+ </ul>+ </nav>++ <select id="maxItemsSelect" onchange="window.location.href = this.options[this.selectedIndex].dataset.url">+ {paginationView.itemsPerPageSelector}+ </select>++ |]++styledPaginationPageLinkDefault :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html+styledPaginationPageLinkDefault _ pagination@Pagination {currentPage} pageUrl pageNumber =+ let+ linkClass = classes [("active", pageNumber == currentPage)]+ in+ [hsx|<li class={linkClass}><a href={pageUrl}>{show pageNumber}</a></li>|]++styledPaginationDotDotDefault :: CSSFramework -> Pagination -> Blaze.Html+styledPaginationDotDotDefault _ _ =+ [hsx|<li><a>…</a></li>|]++styledPaginationItemsPerPageSelectorDefault :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html+styledPaginationItemsPerPageSelectorDefault _ pagination@Pagination {pageSize} itemsPerPageUrl =+ let+ oneOption :: Int -> Blaze.Html+ oneOption n = [hsx|<option value={show n} selected={n == pageSize} data-url={itemsPerPageUrl n}>{n} items per page</option>|]+ in+ [hsx|{forEach [10,20,50,100,200] oneOption}|]++styledPaginationLinkPreviousDefault :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+styledPaginationLinkPreviousDefault _ pagination@Pagination {currentPage} pageUrl =+ let+ prevClass = classes [("disabled", not $ hasPreviousPage pagination)]+ url = if hasPreviousPage pagination then pageUrl else "#"+ in+ [hsx|+ <li class={prevClass}>+ <a href={url} aria-label="Previous">+ <span aria-hidden="true">«</span>+ </a>+ </li>+ |]++styledPaginationLinkNextDefault :: CSSFramework -> Pagination -> ByteString -> Blaze.Html+styledPaginationLinkNextDefault _ pagination@Pagination {currentPage} pageUrl =+ let+ nextClass = classes [("disabled", not $ hasNextPage pagination)]+ url = if hasNextPage pagination then pageUrl else "#"+ in+ [hsx|+ <li class={nextClass}>+ <a href={url} aria-label="Next">+ <span aria-hidden="true">»</span>+ </a>+ </li>+ |]++styledBreadcrumbDefault :: CSSFramework -> [BreadcrumbItem]-> BreadcrumbsView -> Blaze.Html+styledBreadcrumbDefault _ _ breadcrumbsView = [hsx|+ <nav>+ <ol>+ {breadcrumbsView.breadcrumbItems}++ </ol>+ </nav>+|]++styledBreadcrumbItemDefault :: CSSFramework -> [ BreadcrumbItem ]-> BreadcrumbItem -> Bool -> Blaze.Html+styledBreadcrumbItemDefault _ breadcrumbItems breadcrumbItem@BreadcrumbItem {breadcrumbLabel, url} isLast =+ let+ breadcrumbsClasses = classes [("active", isLast)]+ in+ case url of+ Nothing -> [hsx|<li class={breadcrumbsClasses}>{breadcrumbLabel}</li>|]+ Just url -> [hsx|<li class={breadcrumbsClasses}><a href={url}>{breadcrumbLabel}</a></li>|]
@@ -5,7 +5,11 @@ -} module IHP.View.Classes where -import IHP.Prelude+import Prelude hiding (unwords)+import Data.Text (Text, unwords)+import Data.String (IsString(..))+import Data.String.Conversions (cs)+import IHP.HaskellSupport ((|>)) -- | Helper for dynamically generating the @class=".."@ attribute. --
@@ -1,950 +1,14 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE MultiParamTypeClasses #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE InstanceSigs, UndecidableInstances, AllowAmbiguousTypes, ScopedTypeVariables, IncoherentInstances #-} {-| Module: IHP.View.Form-Description: 'IHP.View.Form.formFor' and all form controls+Description: 'IHP.View.Form.FormFor.formFor' and all form controls Copyright: (c) digitally induced GmbH, 2020 -}-module IHP.View.Form where--import IHP.Prelude-import IHP.ValidationSupport-import IHP.HSX.ConvertibleStrings ()-import IHP.ViewErrorMessages ()-import IHP.ViewSupport-import qualified Text.Blaze.Html5 as Html5-import IHP.HSX.ToHtml-import GHC.Types-import IHP.ModelSupport (getModelName, inputValue, isNew, Id', InputValue, didTouchField)-import IHP.HSX.QQ (hsx)-import IHP.View.Types-import IHP.View.Classes ()-import Network.Wai (pathInfo)-import IHP.Controller.Context---- | Forms usually begin with a 'formFor' expression.------ This is how a simple form can look like:------ > renderForm :: Post -> Html--- > renderForm post = formFor post [hsx|--- > {textField #title}--- > {textareaField #body}--- > {submitButton}--- > |]------ Calling this form from inside your HSX code will lead to the following HTML being generated:------ > <form method="POST" action="/CreatePost" id="" class="new-form">--- > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Title</label>--- > <input type="text" name="title" id="post_title" class="form-control" />--- > </div>--- >--- > <div class="form-group" id="form-group-post_body">--- > <label for="post_body">Body</label>--- > <textarea name="body" id="post_body" class="form-control"></textarea>--- > </div>--- >--- > <button class="btn btn-primary">Create Post</button>--- > </form>------ You can see that the form is submitted via @POST@. The form action has also been set by default to @/CreatePost@.------ All inputs have auto-generated class names and ids for styling. Also, all @name@ attributes are set as expected.------ __Field Values:__------ A form control is always filled with the value of the given field when rendering. For example, given a post------ > let post = Post { ..., title = "Hello World" }------ Rendering this, the input value will be set like:------ >>> {textField #title}--- <input ... value="Hello World" />------ __Validation:__------ When rendering a record that has failed validation, the validation error message will be rendered automatically.------ Given a post like this:------ > let post = Post { ..., title = "" }--- > |> validateField #title nonEmpty------ Rendering @{textField #title}@, the input will have the css class @is-invalid@ and an element with the error message will be rendered below the input:------ > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Title</label>--- > <input--- > type="text"--- > name="title"--- > placeholder=""--- > id="post_title"--- > class="form-control is-invalid "--- > />--- > <div class="invalid-feedback">This field cannot be empty</div>--- > </div>-formFor :: forall record. (- ?context :: ControllerContext- , ModelFormAction record- , HasField "meta" record MetaBag- ) => record -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html-formFor record formBody = formForWithOptions @record record (\c -> c) formBody-{-# INLINE formFor #-}---- | Like 'formFor' but allows changing the underlying 'FormContext'------ This is how you can render a form with a @id="post-form"@ id attribute and a custom @data-post-id@ attribute:------ > renderForm :: Post -> Html--- > renderForm post = formForWithOptions formOptions post [hsx|--- > {textField #title}--- > {textareaField #body}--- > {submitButton}--- > |]--- >--- > formOptions :: FormContext Post -> FormContext Post--- > formOptions formContext = formContext--- > |> set #formId "post-form"--- > |> set #customFormAttributes [("data-post-id", show formContext.model.id)]----formForWithOptions :: forall record. (- ?context :: ControllerContext- , ModelFormAction record- , HasField "meta" record MetaBag- ) => record -> (FormContext record -> FormContext record) -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html-formForWithOptions record applyOptions formBody = buildForm (applyOptions (createFormContext record) { formAction = modelFormAction record }) formBody-{-# INLINE formForWithOptions #-}---- | Like 'formFor' but disables the IHP javascript helpers.------ Use it like this:------ > renderForm :: Post -> Html--- > renderForm post = formForWithoutJavascript post [hsx|--- > {textField #title}--- > {textareaField #body}--- > {submitButton}--- > |]------ If you want to use this with e.g. a custom form action, remember that 'formForWithoutJavascript' is just a shortcut for 'formForWithOptions':------ > renderForm :: Post -> Html--- > renderForm post = formForWithOptions formOptions post [hsx|--- > {textField #title}--- > {textareaField #body}--- > {submitButton}--- > |]--- >--- > formOptions :: FormContext Post -> FormContext Post--- > formOptions formContext = formContext--- > |> set #formAction (pathTo BespokeNewPostAction)--- > |> set #disableJavascriptSubmission True----formForWithoutJavascript :: forall record. (- ?context :: ControllerContext- , ModelFormAction record- , HasField "meta" record MetaBag- ) => record -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html-formForWithoutJavascript record formBody = formForWithOptions @record record (\formContext -> formContext { disableJavascriptSubmission = True }) formBody-{-# INLINE formForWithoutJavascript #-}---- | Allows a custom form action (form submission url) to be set------ The URL where the form is going to be submitted to is specified in HTML using the form's @action@ attribute. When using 'formFor' the @action@ attribute is automatically set to the expected path.------ E.g. given the below 'formFor' code, the @action@ is set to @/CreatePost@ or @/UpdatePost@:------ > renderForm :: Post -> Html--- > renderForm post = formFor post [hsx|--- > {textField #title}--- > {textareaField #body}--- > {submitButton}--- > |]------ To override the auto-generated @action@ attribute use the 'formFor\'' function:------ > renderForm :: Post -> Html--- > renderForm post = formFor' post "/my-custom-endpoint" [hsx||]------ If you pass an action to that, you need to wrap it with 'pathTo':------ > renderForm :: Post -> Html--- > renderForm post = formFor' post (pathTo CreateDraftAction) [hsx||]----formFor' :: forall record. (- ?context :: ControllerContext- , HasField "meta" record MetaBag- ) => record -> Text -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html-formFor' record action = buildForm (createFormContext record) { formAction = action }-{-# INLINE formFor' #-}---- | Used by 'formFor' to make a new form context-createFormContext :: forall record. (- ?context :: ControllerContext- , HasField "meta" record MetaBag- ) => record -> FormContext record-createFormContext record =- FormContext- { model = record- , formAction = ""- , formMethod = "POST"- , cssFramework = theCSSFramework- , formId = ""- , formClass = if isNew record then "new-form" else "edit-form"- , customFormAttributes = []- , disableJavascriptSubmission = False- , fieldNamePrefix = ""- }-{-# INLINE createFormContext #-}---- | Used by 'formFor' to render the form-buildForm :: forall model. (?context :: ControllerContext) => FormContext model -> ((?context :: ControllerContext, ?formContext :: FormContext model) => Html5.Html) -> Html5.Html-buildForm formContext inner = [hsx|- <form- method={formContext.formMethod}- action={formContext.formAction}- id={formContext.formId}- class={formContext.formClass}- data-disable-javascript-submission={formContext.disableJavascriptSubmission}- {...formContext.customFormAttributes}- >- {formInner}- </form>- |]- where- formInner = let ?formContext = formContext in inner-{-# INLINE buildForm #-}--nestedFormFor :: forall fieldName childRecord parentRecord idType. (- ?context :: ControllerContext- , ?formContext :: FormContext parentRecord- , HasField fieldName parentRecord [childRecord]- , KnownSymbol fieldName- , KnownSymbol (GetModelName childRecord)- , HasField "id" childRecord idType- , InputValue idType- , HasField "meta" childRecord MetaBag- ) => Proxy fieldName -> ((?context :: ControllerContext, ?formContext :: FormContext childRecord) => Html5.Html) -> Html5.Html-nestedFormFor field nestedRenderForm = forEach children renderChild- where- parentFormContext :: FormContext parentRecord- parentFormContext = ?formContext-- renderChild :: childRecord -> Html5.Html- renderChild record = let ?formContext = buildNestedFormContext record in [hsx|- {hiddenField #id}- {nestedRenderForm}- |]-- buildNestedFormContext :: childRecord -> FormContext childRecord- buildNestedFormContext record = parentFormContext { model = record, fieldNamePrefix = symbolToText @fieldName <> "_" }-- children :: [childRecord]- children = getField @fieldName ?formContext.model-{-# INLINE nestedFormFor #-}---- | Renders a submit button------ > <button class="btn btn-primary">Create Post</button>------ __Example:__------ > renderForm :: Post -> Html--- > renderForm post = formFor post [hsx|--- > {submitButton}--- > |]------ This will generate code like this:------ > <form method="POST" action="/CreatePost" id="" class="new-form">--- > <button class="btn btn-primary">Create Post</button>--- > </form>------ __Custom Text__------ > renderForm :: Post -> Html--- > renderForm post = formFor post [hsx|--- > {submitButton { label = "Create it!" } }--- > |]------ This will generate code like this:------ > <form method="POST" action="/CreatePost" id="" class="new-form">--- > <button class="btn btn-primary">Create it!</button>--- > </form>------ __Custom Class__------ > renderForm :: Post -> Html--- > renderForm post = formFor post [hsx|--- > {submitButton { buttonClass = "create-button" } }--- > |]------ This will generate code like this:------ > <form method="POST" action="/CreatePost" id="" class="new-form">--- > <button class="btn btn-primary create-button">Create Post</button>--- > </form>------ __Disabled button__------ > renderForm :: Post -> Html--- > renderForm post = formFor post [hsx|--- > {submitButton { buttonDisabled = True } }--- > |]------ This will generate code like this:------ > <form method="POST" action="/CreatePost" id="" class="new-form">--- > <button class="btn btn-primary create-button" disabled="disabled">Create Post</button>--- > </form>-submitButton :: forall model. (?formContext :: FormContext model, HasField "meta" model MetaBag, KnownSymbol (GetModelName model)) => SubmitButton-submitButton =- let- modelName = IHP.ModelSupport.getModelName @model- buttonText = modelName |> humanize -- We do this to turn 'Create ProjectTask' into 'Create Project Task'- isNew = IHP.ModelSupport.isNew (model ?formContext)- in SubmitButton- { label = cs $ (if isNew then "Create " else "Save ") <> buttonText- , buttonClass = mempty- , buttonDisabled = False- , cssFramework = ?formContext.cssFramework- }-{-# INLINE submitButton #-}---- | Renders a text input field------ >>> {textField #title}--- <div class="form-group" id="form-group-post_title">--- <label for="post_title">Title</label>--- <input type="text" name="title" id="post_title" class="form-control" />--- </div>------ __Example:__------ > renderForm :: Post -> Html--- > renderForm post = formFor post [hsx|--- > {textField #title}--- > |]------ This will generate code like this:------ > <form method="POST" action="/CreatePost" id="" class="new-form">--- > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Title</label>--- > <input type="text" name="title" id="post_title" class="form-control" />--- > </div>--- > </form>------ __Help Texts:__------ You can add a help text below a form control like this:------ > {(textField #title) { helpText = "Max. 140 characters"} }------ This will generate code like this:------ > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Title</label>--- >--- > <input type="text" name="title" id="post_title" class="form-control" />--- > <small class="form-text text-muted">Max. 140 characters</small>--- > </div>--------- __Custom Field Label Text:__------ By default, the field name will be used as a label text. The camel case field name will be made more human-readable of course, so @contactName@ will turn to @Contact Name@, etc. Sometimes you want to change this auto-generated input label to something custom. Use @fieldLabel@ for that, like this:------ > {(textField #title) { fieldLabel = "Post Title"} }------ This will generate code like this:------ > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Post Title</label>--- > <input type="text" name="title" id="post_title" class="form-control" />--- > </div>--------- __Custom CSS Classes:__------ You can add custom CSS classes to the input and label for better styling. Set @fieldClass@ for adding a class to the input element and @labelClass@ for the label element:------ > {(textField #title) { fieldClass="title-input", labelClass = "title-label" } }------ This will generate code like this:------ > <div class="form-group" id="form-group-post_title">--- > <label class="title-label" for="post_title">Title</label>--- > <input--- > type="text"--- > name="title"--- > id="post_title"--- > class="form-control title-input"--- > />--- > </div>------ Of course, the CSS classes for validation are still set as expected.------ __Placeholder:__------ > {(textField #title) { placeholder = "Enter your title ..." } }------ This will generate code like this:------ > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Title</label>--- >--- > <input--- > type="text"--- > name="title"--- > id="post_title"--- > placeholder="Enter your title ..."--- > class="form-control"--- > />--- > </div>--------- __Required Fields:__------ You can mark an input as required like this:------ > {(textField #title) { required = True } }------ This will generate code like this:------ > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Title</label>--- >--- > <input--- > type="text"--- > name="title"--- > id="post_title"--- > required="required"--- > class="form-control"--- > />--- > </div>------ __Autofocus:__------ You can mark an input with autofocus, to ensure it will be given the input focus on page load, like this:------ > {(textField #title) { autofocus = True } }------ This will generate code like this:------ > <div class="form-group" id="form-group-post_title">--- > <label for="post_title">Title</label>--- >--- > <input--- > type="text"--- > name="title"--- > id="post_title"--- > autofocus="autofocus"--- > class="form-control"--- > />--- > </div>-textField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-textField field = FormField- { fieldType = TextInput- , fieldName = ?formContext.fieldNamePrefix <> cs fieldName- , fieldLabel = fieldNameToFieldLabel (cs fieldName)- , fieldValue = inputValue ((getField @fieldName model) :: value)- , fieldInputId = cs (lcfirst (getModelName @model) <> "_" <> cs fieldName)- , validatorResult = getValidationViolation field model- , fieldClass = ""- , labelClass = ""- , disabled = False- , disableLabel = False- , disableGroup = False- , disableValidationResult = False- , additionalAttributes = []- , cssFramework = ?formContext.cssFramework- , helpText = ""- , placeholder = ""- , required = False- , autofocus = False- }- where- fieldName = symbolVal field- FormContext { model } = ?formContext-{-# INLINE textField #-}---- | Renders a number input field------ >>> {numberField #maxUsers}--- <div class="form-group" id="form-group-company_max_users">--- <label for="company_max_users">Max Users</label>--- <input type="number" name="maxUsers" id="company_maxUsers" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-numberField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-numberField field = (textField field) { fieldType = NumberInput }-{-# INLINE numberField #-}---- | Renders a URL input field------ >>> {urlField #url}--- <div class="form-group" id="form-group-company_url">--- <label for="company_url">Url</label>--- <input type="url" name="url" id="company_url" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-urlField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-urlField field = (textField field) { fieldType = UrlInput }-{-# INLINE urlField #-}---- | Renders a textarea------ >>> {textareaField #body}--- <div class="form-group" id="form-group-post_body">--- <label for="post_body">Body</label>--- <textarea name="body" id="post_body" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-textareaField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-textareaField field = (textField field) { fieldType = TextareaInput }-{-# INLINE textareaField #-}---- | Renders a color field------ >>> {colorField #color}--- <div class="form-group" id="form-group-post_color">--- <label for="post_color">Color</label>--- <input type="color" name="color" id="post_color" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-colorField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-colorField field = (textField field) { fieldType = ColorInput }-{-# INLINE colorField #-}----- | Renders an email field------ >>> {emailField #email}--- <div class="form-group" id="form-group-user_email">--- <label for="user_email">Email</label>--- <input type="email" name="email" id="user_email" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-emailField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , KnownSymbol (GetModelName model)- , InputValue value- ) => Proxy fieldName -> FormField-emailField field = (textField field) { fieldType = EmailInput }-{-# INLINE emailField #-}---- | Renders an date field------ >>> {dateField #createdAt}--- <div class="form-group" id="form-group-user_created_at">--- <label for="user_createdAt">Created At</label>--- <input type="date" name="createdAt" id="user_createdAt" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-dateField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-dateField field = (textField field) { fieldType = DateInput }-{-# INLINE dateField #-}---- | Renders an password field------ >>> {passwordField #password}--- <div class="form-group" id="form-group-user_password">--- <label for="user_password">Password</label>--- <input type="password" name="password" id="user_password" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-passwordField :: forall fieldName model.- ( ?formContext :: FormContext model- , HasField fieldName model Text- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-passwordField field = (textField field) { fieldType = PasswordInput }-{-# INLINE passwordField #-}---- | Renders an date-time field------ >>> {dateTimeField #createdAt}--- <div class="form-group" id="form-group-user_created_at">--- <label for="user_createdAt">Created At</label>--- <input type="datetime-local" name="createdAt" id="user_createdAt" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-dateTimeField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-dateTimeField alpha = (textField alpha) { fieldType = DateTimeInput }-{-# INLINE dateTimeField #-}---- | Renders an hidden field------ >>> {hiddenField #projectId}--- <input type="hidden" name="projectId" id="checkoutSession_projectId" class="form-control" />------ The hidden field is by default rendered without a form group and without a label.-hiddenField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-hiddenField field = (textField field) { fieldType = HiddenInput, disableLabel = True, disableGroup = True, disableValidationResult = True }-{-# INLINE hiddenField #-}---- | Renders an file field------ >>> {fileField #profilePicture}--- <input type="file" name="profilePicture" id="user_profilePicture" class="form-control" />------ See 'textField' for examples of possible form control options.-fileField :: forall fieldName model value.- ( ?formContext :: FormContext model- , HasField fieldName model value- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue value- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-fileField field = (textField field) { fieldType = FileInput }-{-# INLINE fileField #-}---- | Renders a checkbox field------ >>> {checkboxField #active}--- <div class="form-group" id="form-group-user_active">--- <label for="user_active">Active</label>--- <input type="checkbox" name="active" id="user_active" class="form-control" />--- </div>------ See 'textField' for examples of possible form control options.-checkboxField :: forall fieldName model.- ( ?formContext :: FormContext model- , HasField fieldName model Bool- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> FormField-checkboxField field = FormField- { fieldType = CheckboxInput- , fieldName = cs fieldName- , fieldLabel = fieldNameToFieldLabel (cs fieldName)- , fieldValue = if getField @fieldName model then "yes" else "no"- , fieldInputId = cs (lcfirst (getModelName @model) <> "_" <> cs fieldName)- , validatorResult = getValidationViolation field model- , fieldClass = ""- , labelClass = ""- , disabled = False- , disableLabel = False- , disableGroup = False- , disableValidationResult = False- , additionalAttributes = []- , cssFramework = ?formContext.cssFramework- , helpText = ""- , placeholder = ""- , required = False- , autofocus = False- }- where- fieldName = symbolVal field- FormContext { model } = ?formContext-{-# INLINE checkboxField #-}---- | Select inputs require you to pass a list of possible values to select.------ > formFor project [hsx|--- > {selectField #userId users}--- > |]------ In the example above the variable users contains all the possible option values for the select.------ You also need to define a instance @CanSelect User@:------ > instance CanSelect User where--- > -- Here we specify that the <option> value should contain a `Id User`--- > type SelectValue User = Id User--- > -- Here we specify how to transform the model into <option>-value--- > selectValue user = user.id--- > -- And here we specify the <option>-text--- > selectLabel user = user.name------ Given the above example, the rendered form will look like this:------ > <!-- Assuming: users = [User { id = 1, name = "Marc" }, User { id = 2, name = "Andreas" }] -->--- > <form ...>--- > <select name="user_id">--- > <option value="1">Marc</option>--- > <option value="2">Andreas</option>--- > </select>--- > </form>------ If you want a certain value to be preselected, set the value in the controller. For example, to have the first user be preselected in the above example:------ > action NewProjectAction = do--- > users <- query @User |> fetch--- > let userId = headMay users |> maybe def (.id)--- > let target = newRecord @Project |> set #userId userId--- > render NewView { .. }-selectField :: forall fieldName model item.- ( ?formContext :: FormContext model- , HasField fieldName model (SelectValue item)- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , KnownSymbol (GetModelName model)- , CanSelect item- , InputValue (SelectValue item)- , Typeable model- , Eq (SelectValue item)- ) => Proxy fieldName -> [item] -> FormField-selectField field items = FormField- { fieldType =- let- itemToTuple :: item -> (Text, Text)- itemToTuple item = (selectLabel item, inputValue (selectValue item))- in- SelectInput (map itemToTuple items)- , fieldName = cs fieldName- , fieldLabel = removeIdSuffix $ fieldNameToFieldLabel (cs fieldName)- -- If the field is not touched, we don't want to render the value from the model- -- so we force the user to select. If a value was explicitely set in the model, we- -- render that value.- , fieldValue = if IHP.ModelSupport.didTouchField field model || (not $ isNew model)- then inputValue (getField @fieldName model :: SelectValue item)- else ""- , fieldInputId = cs (lcfirst (getModelName @model) <> "_" <> cs fieldName)- , validatorResult = getValidationViolation field model- , fieldClass = ""- , labelClass = ""- , disabled = False- , disableLabel = False- , disableGroup = False- , disableValidationResult = False- , additionalAttributes = []- , cssFramework = ?formContext.cssFramework- , helpText = ""- , placeholder = "Please select"- , required = False- , autofocus = False- }- where- fieldName = symbolVal field- FormContext { model } = ?formContext-{-# INLINE selectField #-}---- | Radio require you to pass a list of possible values to select. We use the same mechanism as for for 'selectField'.------ > formFor project [hsx|--- > {radioField #userId users}--- > |]------ In the example above the variable users contains all the possible option values for the radios.------ You also need to define a instance @CanSelect User@:------ > instance CanSelect User where--- > -- Here we specify that the <option> value should contain a `Id User`--- > type SelectValue User = Id User--- > -- Here we specify how to transform the model into <option>-value--- > selectValue user = user.id--- > -- And here we specify the <option>-text--- > selectLabel user = user.name------ Given the above example, the rendered form will look like this (omitting classes for brevity):------ > <!-- Assuming: users = [User { id = 1, name = "Marc" }, User { id = 2, name = "Andreas" }] -->--- > <form ...>--- > <fieldset>--- > <div>--- > <input type="radio" id="option1" value="1"/>--- > <label for="option1">Marc</label>--- > </div>--- > <div>--- > <input type="radio" id="option2" value="2"/>--- > <label for="option2">Andreas</label>--- > </div>--- > </fieldset>--- > </form>------ If you want a certain value to be preselected, set the value in the controller. For example, to have the first user be preselected in the above example:------ > action NewProjectAction = do--- > users <- query @User |> fetch--- > let userId = headMay users |> maybe def (.id)--- > let target = newRecord @Project |> set #userId userId--- > render NewView { .. }-radioField :: forall fieldName model item.- ( ?formContext :: FormContext model- , HasField fieldName model (SelectValue item)- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , KnownSymbol (GetModelName model)- , CanSelect item- , InputValue (SelectValue item)- , Typeable model- , Eq (SelectValue item)- ) => Proxy fieldName -> [item] -> FormField-radioField field items = (selectField field items)- { fieldType =- let- itemToTuple :: item -> (Text, Text)- itemToTuple item = (selectLabel item, inputValue (selectValue item))- in- RadioInput (map itemToTuple items)- , placeholder = ""- }-{-# INLINE radioField #-}--class CanSelect model where- -- | Here we specify the type of the @<option>@ value, usually an @Id model@- type SelectValue model :: GHC.Types.Type-- -- | Here we specify the <option>-text- selectLabel :: model -> Text- default selectLabel :: Show model => model -> Text- selectLabel = tshow-- -- | Here we specify how to transform the model into @<option>@-value- selectValue :: model -> SelectValue model- default selectValue :: HasField "id" model (SelectValue model) => model -> SelectValue model- selectValue = (.id)--instance ToHtml FormField where- {-# INLINE toHtml #-}- toHtml :: FormField -> Html5.Html- toHtml formField@(FormField { cssFramework }) = styledFormField cssFramework cssFramework formField--instance ToHtml SubmitButton where- {-# INLINE toHtml #-}- toHtml submitButton@(SubmitButton { cssFramework }) = styledSubmitButton cssFramework cssFramework submitButton---- | Returns the form's action attribute for a given record.-class ModelFormAction record where- modelFormAction :: (?context :: ControllerContext) => record -> Text--instance- ( HasField "id" record (Id' (GetTableName record))- , HasField "meta" record MetaBag- , KnownSymbol (GetModelName record)- , Show (Id' (GetTableName record))- ) => ModelFormAction record where- -- | Returns the form's action attribute for a given record.- --- -- Expects that AutoRoute is used. Otherwise you need to use @formFor'@ or specify- -- a manual ModelFormAction instance.- --- -- We guess the form submit action based on the current url- -- It's a @New..Action@ or @Edit..Action@. We guess the corresponding- -- @Create..Action@ name or @Update..Action@ name based on the AutoRoute rules- --- -- In case the routing is not based on AutoRoute, a manual ModelFormAction instance needs- -- to be defined- modelFormAction record =- let- path = theRequest.pathInfo- action = if isNew record- then "Create" <> getModelName @record- else "Update" <> getModelName @record <> "?" <> lcfirst (getModelName @record) <> "Id=" <> tshow record.id- in- init path- |> (\path -> [""] <> (fromMaybe [] path) <> [action])- |> intercalate "/"---- | Renders a validation failure for a field. If the field passed all validation, no error is shown.------ >>> {validationResult #email}--- <div class="invalid-feedback">is not a valid email</div>-validationResult :: forall fieldName model fieldType.- ( ?formContext :: FormContext model- , HasField fieldName model fieldType- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , InputValue fieldType- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> Html-validationResult field = styledValidationResult cssFramework cssFramework (textField field)- where- result = getValidationFailure field model- model = ?formContext.model- cssFramework = ?formContext.cssFramework+module IHP.View.Form+ ( module IHP.View.Form.FormFor+ , module IHP.View.Form.Fields+ , module IHP.View.Form.Select+ ) where --- | Returns the validation failure for a field. If the field passed all validation, this returns 'Nothing'.------ >>> {validationResultMaybe #email}--- Just "is not a valid email"-validationResultMaybe :: forall fieldName model fieldType.- ( ?formContext :: FormContext model- , HasField fieldName model fieldType- , HasField "meta" model MetaBag- , KnownSymbol fieldName- , KnownSymbol (GetModelName model)- ) => Proxy fieldName -> Maybe Text-validationResultMaybe field = getValidationFailure field ?formContext.model+import IHP.View.Form.FormFor+import IHP.View.Form.Fields+import IHP.View.Form.Select
@@ -0,0 +1,521 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE IncoherentInstances #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-|+Module: IHP.View.Form.Fields+Description: Form field controls (text, number, checkbox, etc.)+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.View.Form.Fields where++import IHP.HSX.ConvertibleStrings ()+import IHP.HSX.ToHtml+import IHP.ModelSupport (InputValue, getModelName, inputValue)+import IHP.Prelude+import IHP.ValidationSupport+import IHP.View.Classes ()+import IHP.View.Types+import IHP.ViewSupport+import qualified Text.Blaze.Html5 as Html5++-- | Renders a text input field+--+-- >>> {textField #title}+-- <div class="form-group" id="form-group-post_title">+-- <label for="post_title">Title</label>+-- <input type="text" name="title" id="post_title" class="form-control" />+-- </div>+--+-- __Example:__+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor post [hsx|+-- > {textField #title}+-- > |]+--+-- This will generate code like this:+--+-- > <form method="POST" action="/CreatePost" id="" class="new-form">+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Title</label>+-- > <input type="text" name="title" id="post_title" class="form-control" />+-- > </div>+-- > </form>+--+-- __Help Texts:__+--+-- You can add a help text below a form control like this:+--+-- > {(textField #title) { helpText = "Max. 140 characters"} }+--+-- This will generate code like this:+--+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Title</label>+-- >+-- > <input type="text" name="title" id="post_title" class="form-control" />+-- > <small class="form-text text-muted">Max. 140 characters</small>+-- > </div>+--+--+-- __Custom Field Label Text:__+--+-- By default, the field name will be used as a label text. The camel case field name will be made more human-readable of course, so @contactName@ will turn to @Contact Name@, etc. Sometimes you want to change this auto-generated input label to something custom. Use @fieldLabel@ for that, like this:+--+-- > {(textField #title) { fieldLabel = "Post Title"} }+--+-- This will generate code like this:+--+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Post Title</label>+-- > <input type="text" name="title" id="post_title" class="form-control" />+-- > </div>+--+--+-- __Custom CSS Classes:__+--+-- You can add custom CSS classes to the input and label for better styling. Set @fieldClass@ for adding a class to the input element and @labelClass@ for the label element:+--+-- > {(textField #title) { fieldClass="title-input", labelClass = "title-label" } }+--+-- This will generate code like this:+--+-- > <div class="form-group" id="form-group-post_title">+-- > <label class="title-label" for="post_title">Title</label>+-- > <input+-- > type="text"+-- > name="title"+-- > id="post_title"+-- > class="form-control title-input"+-- > />+-- > </div>+--+-- Of course, the CSS classes for validation are still set as expected.+--+-- __Placeholder:__+--+-- > {(textField #title) { placeholder = "Enter your title ..." } }+--+-- This will generate code like this:+--+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Title</label>+-- >+-- > <input+-- > type="text"+-- > name="title"+-- > id="post_title"+-- > placeholder="Enter your title ..."+-- > class="form-control"+-- > />+-- > </div>+--+--+-- __Required Fields:__+--+-- You can mark an input as required like this:+--+-- > {(textField #title) { required = True } }+--+-- This will generate code like this:+--+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Title</label>+-- >+-- > <input+-- > type="text"+-- > name="title"+-- > id="post_title"+-- > required="required"+-- > class="form-control"+-- > />+-- > </div>+--+-- __Autofocus:__+--+-- You can mark an input with autofocus, to ensure it will be given the input focus on page load, like this:+--+-- > {(textField #title) { autofocus = True } }+--+-- This will generate code like this:+--+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Title</label>+-- >+-- > <input+-- > type="text"+-- > name="title"+-- > id="post_title"+-- > autofocus="autofocus"+-- > class="form-control"+-- > />+-- > </div>+textField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+textField field = FormField+ { fieldType = TextInput+ , fieldName = ?formContext.fieldNamePrefix <> cs fieldName+ , fieldLabel = fieldNameToFieldLabel (cs fieldName)+ , fieldValue = inputValue ((getField @fieldName model) :: value)+ , fieldInputId = cs (lcfirst (getModelName @model) <> "_" <> cs fieldName)+ , validatorResult = getValidationViolation field model+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = ?formContext.cssFramework+ , helpText = ""+ , placeholder = ""+ , required = False+ , autofocus = False+ }+ where+ fieldName = symbolVal field+ FormContext { model } = ?formContext+{-# INLINE textField #-}++-- | Renders a number input field+--+-- >>> {numberField #maxUsers}+-- <div class="form-group" id="form-group-company_max_users">+-- <label for="company_max_users">Max Users</label>+-- <input type="number" name="maxUsers" id="company_maxUsers" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+numberField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+numberField field = (textField field) { fieldType = NumberInput }+{-# INLINE numberField #-}++-- | Renders a URL input field+--+-- >>> {urlField #url}+-- <div class="form-group" id="form-group-company_url">+-- <label for="company_url">Url</label>+-- <input type="url" name="url" id="company_url" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+urlField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+urlField field = (textField field) { fieldType = UrlInput }+{-# INLINE urlField #-}++-- | Renders a textarea+--+-- >>> {textareaField #body}+-- <div class="form-group" id="form-group-post_body">+-- <label for="post_body">Body</label>+-- <textarea name="body" id="post_body" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+textareaField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+textareaField field = (textField field) { fieldType = TextareaInput }+{-# INLINE textareaField #-}++-- | Renders a color field+--+-- >>> {colorField #color}+-- <div class="form-group" id="form-group-post_color">+-- <label for="post_color">Color</label>+-- <input type="color" name="color" id="post_color" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+colorField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+colorField field = (textField field) { fieldType = ColorInput }+{-# INLINE colorField #-}+++-- | Renders an email field+--+-- >>> {emailField #email}+-- <div class="form-group" id="form-group-user_email">+-- <label for="user_email">Email</label>+-- <input type="email" name="email" id="user_email" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+emailField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , KnownSymbol (GetModelName model)+ , InputValue value+ ) => Proxy fieldName -> FormField+emailField field = (textField field) { fieldType = EmailInput }+{-# INLINE emailField #-}++-- | Renders a date field+--+-- >>> {dateField #createdAt}+-- <div class="form-group" id="form-group-user_created_at">+-- <label for="user_createdAt">Created At</label>+-- <input type="date" name="createdAt" id="user_createdAt" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+--+-- Use @additionalAttributes@ and set @data-*@ attributes to control the flatpickr options.+--+-- > {(dateField #createdAt) {+-- > additionalAttributes =+-- > [ ("data-alt-format", "Y-m-d")+-- > , ("data-min-date", "today")+-- > ]+-- > }}+dateField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+dateField field = (textField field) { fieldType = DateInput }+{-# INLINE dateField #-}++-- | Renders a password field+--+-- >>> {passwordField #password}+-- <div class="form-group" id="form-group-user_password">+-- <label for="user_password">Password</label>+-- <input type="password" name="password" id="user_password" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+passwordField :: forall fieldName model.+ ( ?formContext :: FormContext model+ , HasField fieldName model Text+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+passwordField field = (textField field) { fieldType = PasswordInput }+{-# INLINE passwordField #-}++-- | Renders a @\<input type=\"datetime-local\"\/>@ field.+--+-- The field value is automatically formatted as @YYYY-MM-DDTHH:MM@ using+-- the 'DateTimeValue' typeclass, which ensures only 'UTCTime', 'LocalTime',+-- and their 'Maybe' variants are accepted. Using this on an incompatible type+-- (e.g. 'Bool') will produce a compile error.+--+-- >>> {dateTimeField #createdAt}+-- <div class="form-group" id="form-group-user_created_at">+-- <label for="user_createdAt">Created At</label>+-- <input type="datetime-local" name="createdAt" id="user_createdAt" class="form-control" />+-- </div>+--+-- The corresponding 'ParamReader' instances accept multiple formats on submission:+--+-- * @2020-11-08T12:03:35Z@ (ISO 8601 with seconds and Z)+-- * @2020-11-08T12:03:35@ (seconds without Z, produced when @step@ is set)+-- * @2020-11-08T12:03@ (default datetime-local, no seconds)+-- * @2020-11-08@ (date only)+--+-- See 'textField' for examples of possible form control options.+--+-- Use @additionalAttributes@ and set @data-*@ attributes to control the flatpickr options.+--+-- > {(dateTimeField #createdAt) {+-- > additionalAttributes =+-- > [ ("data-alt-format", "Y-m-d H:i")+-- > , ("data-min-date", "today")+-- > ]+-- > }}+dateTimeField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , DateTimeValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+dateTimeField alpha =+ (textField alpha)+ { fieldType = DateTimeInput+ , fieldValue = dateTimeValue ((getField @fieldName model) :: value)+ }+ where+ FormContext { model } = ?formContext+{-# INLINE dateTimeField #-}++-- | Provides a way to convert a time value to the format expected by+-- @\<input type=\"datetime-local\"\/>@, i.e. @YYYY-MM-DDTHH:MM@.+--+-- This ensures that 'dateTimeField' only accepts time-like types+-- ('UTCTime', 'LocalTime', and their 'Maybe' variants), giving a+-- compile error if used on unrelated types like 'Bool'.+class DateTimeValue a where+ dateTimeValue :: a -> Text+ default dateTimeValue :: FormatTime a => a -> Text+ dateTimeValue time = cs (formatTime defaultTimeLocale "%Y-%m-%dT%H:%M" time)++instance DateTimeValue UTCTime+instance DateTimeValue LocalTime++instance DateTimeValue a => DateTimeValue (Maybe a) where+ dateTimeValue (Just time) = dateTimeValue time+ dateTimeValue Nothing = ""++-- | Renders a hidden field+--+-- >>> {hiddenField #projectId}+-- <input type="hidden" name="projectId" id="checkoutSession_projectId" class="form-control" />+--+-- The hidden field is by default rendered without a form group and without a label.+hiddenField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+hiddenField field = (textField field) { fieldType = HiddenInput, disableLabel = True, disableGroup = True, disableValidationResult = True }+{-# INLINE hiddenField #-}++-- | Renders a file field+--+-- >>> {fileField #profilePicture}+-- <input type="file" name="profilePicture" id="user_profilePicture" class="form-control" />+--+-- See 'textField' for examples of possible form control options.+fileField :: forall fieldName model value.+ ( ?formContext :: FormContext model+ , HasField fieldName model value+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue value+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+fileField field = (textField field) { fieldType = FileInput }+{-# INLINE fileField #-}++-- | Renders a checkbox field+--+-- >>> {checkboxField #active}+-- <div class="form-group" id="form-group-user_active">+-- <label for="user_active">Active</label>+-- <input type="checkbox" name="active" id="user_active" class="form-control" />+-- </div>+--+-- See 'textField' for examples of possible form control options.+checkboxField :: forall fieldName model.+ ( ?formContext :: FormContext model+ , HasField fieldName model Bool+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> FormField+checkboxField field = FormField+ { fieldType = CheckboxInput+ , fieldName = cs fieldName+ , fieldLabel = fieldNameToFieldLabel (cs fieldName)+ , fieldValue = if getField @fieldName model then "yes" else "no"+ , fieldInputId = cs (lcfirst (getModelName @model) <> "_" <> cs fieldName)+ , validatorResult = getValidationViolation field model+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = ?formContext.cssFramework+ , helpText = ""+ , placeholder = ""+ , required = False+ , autofocus = False+ }+ where+ fieldName = symbolVal field+ FormContext { model } = ?formContext+{-# INLINE checkboxField #-}++instance ToHtml FormField where+ {-# INLINE toHtml #-}+ toHtml :: FormField -> Html5.Html+ toHtml formField@(FormField { cssFramework }) = styledFormField cssFramework cssFramework formField++instance ToHtml SubmitButton where+ {-# INLINE toHtml #-}+ toHtml submitButton@(SubmitButton { cssFramework }) = styledSubmitButton cssFramework cssFramework submitButton++-- | Renders a validation failure for a field. If the field passed all validation, no error is shown.+--+-- >>> {validationResult #email}+-- <div class="invalid-feedback">is not a valid email</div>+validationResult :: forall fieldName model fieldType.+ ( ?formContext :: FormContext model+ , HasField fieldName model fieldType+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , InputValue fieldType+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> Html+validationResult field = styledValidationResult cssFramework cssFramework (textField field)+ where+ result = getValidationFailure field model+ model = ?formContext.model+ cssFramework = ?formContext.cssFramework++-- | Returns the validation failure for a field. If the field passed all validation, this returns 'Nothing'.+--+-- >>> {validationResultMaybe #email}+-- Just "is not a valid email"+validationResultMaybe :: forall fieldName model fieldType.+ ( ?formContext :: FormContext model+ , HasField fieldName model fieldType+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , KnownSymbol (GetModelName model)+ ) => Proxy fieldName -> Maybe Text+validationResultMaybe field = getValidationFailure field ?formContext.model
@@ -0,0 +1,354 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE IncoherentInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-|+Module: IHP.View.Form.FormFor+Description: 'formFor' and form building utilities+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.View.Form.FormFor where++import IHP.Controller.Context+import IHP.HSX.ConvertibleStrings ()+import IHP.HSX.QQ (hsx)+import IHP.ModelSupport (Id', InputValue, getModelName, isNew)+import IHP.Prelude+import IHP.View.Form.Fields (hiddenField)+import IHP.View.Types+import IHP.ViewSupport+import Network.Wai (Request, pathInfo)+import qualified Text.Blaze.Html5 as Html5++-- | Forms usually begin with a 'formFor' expression.+--+-- This is how a simple form can look like:+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor post [hsx|+-- > {textField #title}+-- > {textareaField #body}+-- > {submitButton}+-- > |]+--+-- Calling this form from inside your HSX code will lead to the following HTML being generated:+--+-- > <form method="POST" action="/CreatePost" id="" class="new-form">+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Title</label>+-- > <input type="text" name="title" id="post_title" class="form-control" />+-- > </div>+-- >+-- > <div class="form-group" id="form-group-post_body">+-- > <label for="post_body">Body</label>+-- > <textarea name="body" id="post_body" class="form-control"></textarea>+-- > </div>+-- >+-- > <button class="btn btn-primary">Create Post</button>+-- > </form>+--+-- You can see that the form is submitted via @POST@. The form action has also been set by default to @/CreatePost@.+--+-- All inputs have auto-generated class names and ids for styling. Also, all @name@ attributes are set as expected.+--+-- __Field Values:__+--+-- A form control is always filled with the value of the given field when rendering. For example, given a post+--+-- > let post = Post { ..., title = "Hello World" }+--+-- Rendering this, the input value will be set like:+--+-- >>> {textField #title}+-- <input ... value="Hello World" />+--+-- __Validation:__+--+-- When rendering a record that has failed validation, the validation error message will be rendered automatically.+--+-- Given a post like this:+--+-- > let post = Post { ..., title = "" }+-- > |> validateField #title nonEmpty+--+-- Rendering @{textField #title}@, the input will have the css class @is-invalid@ and an element with the error message will be rendered below the input:+--+-- > <div class="form-group" id="form-group-post_title">+-- > <label for="post_title">Title</label>+-- > <input+-- > type="text"+-- > name="title"+-- > placeholder=""+-- > id="post_title"+-- > class="form-control is-invalid "+-- > />+-- > <div class="invalid-feedback">This field cannot be empty</div>+-- > </div>+formFor :: forall record. (+ ?context :: ControllerContext+ , ?request :: Request+ , ModelFormAction record+ , HasField "meta" record MetaBag+ ) => record -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html+formFor record formBody = formForWithOptions @record record (\c -> c) formBody+{-# INLINE formFor #-}++-- | Like 'formFor' but allows changing the underlying 'FormContext'+--+-- This is how you can render a form with a @id="post-form"@ id attribute and a custom @data-post-id@ attribute:+--+-- > renderForm :: Post -> Html+-- > renderForm post = formForWithOptions formOptions post [hsx|+-- > {textField #title}+-- > {textareaField #body}+-- > {submitButton}+-- > |]+-- >+-- > formOptions :: FormContext Post -> FormContext Post+-- > formOptions formContext = formContext+-- > |> set #formId "post-form"+-- > |> set #customFormAttributes [("data-post-id", show formContext.model.id)]+--+formForWithOptions :: forall record. (+ ?context :: ControllerContext+ , ?request :: Request+ , ModelFormAction record+ , HasField "meta" record MetaBag+ ) => record -> (FormContext record -> FormContext record) -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html+formForWithOptions record applyOptions formBody = buildForm (applyOptions (createFormContext record) { formAction = modelFormAction record }) formBody+{-# INLINE formForWithOptions #-}++-- | Like 'formFor' but disables the IHP javascript helpers.+--+-- Use it like this:+--+-- > renderForm :: Post -> Html+-- > renderForm post = formForWithoutJavascript post [hsx|+-- > {textField #title}+-- > {textareaField #body}+-- > {submitButton}+-- > |]+--+-- If you want to use this with e.g. a custom form action, remember that 'formForWithoutJavascript' is just a shortcut for 'formForWithOptions':+--+-- > renderForm :: Post -> Html+-- > renderForm post = formForWithOptions formOptions post [hsx|+-- > {textField #title}+-- > {textareaField #body}+-- > {submitButton}+-- > |]+-- >+-- > formOptions :: FormContext Post -> FormContext Post+-- > formOptions formContext = formContext+-- > |> set #formAction (pathTo BespokeNewPostAction)+-- > |> set #disableJavascriptSubmission True+--+formForWithoutJavascript :: forall record. (+ ?context :: ControllerContext+ , ?request :: Request+ , ModelFormAction record+ , HasField "meta" record MetaBag+ ) => record -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html+formForWithoutJavascript record formBody = formForWithOptions @record record (\formContext -> formContext { disableJavascriptSubmission = True }) formBody+{-# INLINE formForWithoutJavascript #-}++-- | Allows a custom form action (form submission url) to be set+--+-- The URL where the form is going to be submitted to is specified in HTML using the form's @action@ attribute. When using 'formFor' the @action@ attribute is automatically set to the expected path.+--+-- E.g. given the below 'formFor' code, the @action@ is set to @/CreatePost@ or @/UpdatePost@:+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor post [hsx|+-- > {textField #title}+-- > {textareaField #body}+-- > {submitButton}+-- > |]+--+-- To override the auto-generated @action@ attribute use the 'formFor\'' function:+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor' post "/my-custom-endpoint" [hsx||]+--+-- If you pass an action to that, you need to wrap it with 'pathTo':+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor' post (pathTo CreateDraftAction) [hsx||]+--+formFor' :: forall record. (+ ?context :: ControllerContext+ , ?request :: Request+ , HasField "meta" record MetaBag+ ) => record -> Text -> ((?context :: ControllerContext, ?formContext :: FormContext record) => Html5.Html) -> Html5.Html+formFor' record action = buildForm (createFormContext record) { formAction = action }+{-# INLINE formFor' #-}++-- | Used by 'formFor' to make a new form context+createFormContext :: forall record. (+ ?request :: Request+ , HasField "meta" record MetaBag+ ) => record -> FormContext record+createFormContext record =+ FormContext+ { model = record+ , formAction = ""+ , formMethod = "POST"+ , cssFramework = theCSSFramework+ , formId = ""+ , formClass = if isNew record then "new-form" else "edit-form"+ , customFormAttributes = []+ , disableJavascriptSubmission = False+ , fieldNamePrefix = ""+ }+{-# INLINE createFormContext #-}++-- | Used by 'formFor' to render the form+buildForm :: forall model. (?context :: ControllerContext) => FormContext model -> ((?context :: ControllerContext, ?formContext :: FormContext model) => Html5.Html) -> Html5.Html+buildForm formContext inner = [hsx|+ <form+ method={formContext.formMethod}+ action={formContext.formAction}+ id={formContext.formId}+ class={formContext.formClass}+ data-disable-javascript-submission={formContext.disableJavascriptSubmission}+ {...formContext.customFormAttributes}+ >+ {formInner}+ </form>+ |]+ where+ formInner = let ?formContext = formContext in inner+{-# INLINE buildForm #-}++nestedFormFor :: forall fieldName childRecord parentRecord idType. (+ ?context :: ControllerContext+ , ?formContext :: FormContext parentRecord+ , HasField fieldName parentRecord [childRecord]+ , KnownSymbol fieldName+ , KnownSymbol (GetModelName childRecord)+ , HasField "id" childRecord idType+ , InputValue idType+ , HasField "meta" childRecord MetaBag+ ) => Proxy fieldName -> ((?context :: ControllerContext, ?formContext :: FormContext childRecord) => Html5.Html) -> Html5.Html+nestedFormFor field nestedRenderForm = forEach children renderChild+ where+ parentFormContext :: FormContext parentRecord+ parentFormContext = ?formContext++ renderChild :: childRecord -> Html5.Html+ renderChild record = let ?formContext = buildNestedFormContext record in [hsx|+ {hiddenField #id}+ {nestedRenderForm}+ |]++ buildNestedFormContext :: childRecord -> FormContext childRecord+ buildNestedFormContext record = parentFormContext { model = record, fieldNamePrefix = symbolToText @fieldName <> "_" }++ children :: [childRecord]+ children = getField @fieldName ?formContext.model+{-# INLINE nestedFormFor #-}++-- | Renders a submit button+--+-- > <button class="btn btn-primary">Create Post</button>+--+-- __Example:__+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor post [hsx|+-- > {submitButton}+-- > |]+--+-- This will generate code like this:+--+-- > <form method="POST" action="/CreatePost" id="" class="new-form">+-- > <button class="btn btn-primary">Create Post</button>+-- > </form>+--+-- __Custom Text__+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor post [hsx|+-- > {submitButton { label = "Create it!" } }+-- > |]+--+-- This will generate code like this:+--+-- > <form method="POST" action="/CreatePost" id="" class="new-form">+-- > <button class="btn btn-primary">Create it!</button>+-- > </form>+--+-- __Custom Class__+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor post [hsx|+-- > {submitButton { buttonClass = "create-button" } }+-- > |]+--+-- This will generate code like this:+--+-- > <form method="POST" action="/CreatePost" id="" class="new-form">+-- > <button class="btn btn-primary create-button">Create Post</button>+-- > </form>+--+-- __Disabled button__+--+-- > renderForm :: Post -> Html+-- > renderForm post = formFor post [hsx|+-- > {submitButton { buttonDisabled = True } }+-- > |]+--+-- This will generate code like this:+--+-- > <form method="POST" action="/CreatePost" id="" class="new-form">+-- > <button class="btn btn-primary create-button" disabled="disabled">Create Post</button>+-- > </form>+submitButton :: forall model. (?formContext :: FormContext model, HasField "meta" model MetaBag, KnownSymbol (GetModelName model)) => SubmitButton+submitButton =+ let+ modelName = IHP.ModelSupport.getModelName @model+ buttonText = modelName |> humanize -- We do this to turn 'Create ProjectTask' into 'Create Project Task'+ isNew = IHP.ModelSupport.isNew (model ?formContext)+ in SubmitButton+ { label = cs $ (if isNew then "Create " else "Save ") <> buttonText+ , buttonClass = mempty+ , buttonDisabled = False+ , cssFramework = ?formContext.cssFramework+ }+{-# INLINE submitButton #-}++-- | Returns the form's action attribute for a given record.+class ModelFormAction record where+ modelFormAction :: (?context :: ControllerContext, ?request :: Request) => record -> Text++instance+ ( HasField "id" record (Id' (GetTableName record))+ , HasField "meta" record MetaBag+ , KnownSymbol (GetModelName record)+ , Show (Id' (GetTableName record))+ ) => ModelFormAction record where+ -- | Returns the form's action attribute for a given record.+ --+ -- Expects that AutoRoute is used. Otherwise you need to use @formFor'@ or specify+ -- a manual ModelFormAction instance.+ --+ -- We guess the form submit action based on the current url+ -- It's a @New..Action@ or @Edit..Action@. We guess the corresponding+ -- @Create..Action@ name or @Update..Action@ name based on the AutoRoute rules+ --+ -- In case the routing is not based on AutoRoute, a manual ModelFormAction instance needs+ -- to be defined+ modelFormAction record =+ let+ path = theRequest.pathInfo+ action = if isNew record+ then "Create" <> getModelName @record+ else "Update" <> getModelName @record <> "?" <> lcfirst (getModelName @record) <> "Id=" <> tshow record.id+ in+ init path+ |> (\path -> [""] <> (fromMaybe [] path) <> [action])+ |> intercalate "/"
@@ -0,0 +1,182 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE IncoherentInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+{-|+Module: IHP.View.Form.Select+Description: Select and radio form controls+Copyright: (c) digitally induced GmbH, 2020+-}+module IHP.View.Form.Select where++import GHC.Types+import IHP.HSX.ConvertibleStrings ()+import IHP.ModelSupport (InputValue, didTouchField, getModelName, inputValue, isNew)+import IHP.Prelude+import IHP.ValidationSupport+import IHP.View.Classes ()+import IHP.View.Types++-- | Select inputs require you to pass a list of possible values to select.+--+-- > formFor project [hsx|+-- > {selectField #userId users}+-- > |]+--+-- In the example above the variable users contains all the possible option values for the select.+--+-- You also need to define a instance @CanSelect User@:+--+-- > instance CanSelect User where+-- > -- Here we specify that the <option> value should contain a `Id User`+-- > type SelectValue User = Id User+-- > -- Here we specify how to transform the model into <option>-value+-- > selectValue user = user.id+-- > -- And here we specify the <option>-text+-- > selectLabel user = user.name+--+-- Given the above example, the rendered form will look like this:+--+-- > <!-- Assuming: users = [User { id = 1, name = "Marc" }, User { id = 2, name = "Andreas" }] -->+-- > <form ...>+-- > <select name="user_id">+-- > <option value="1">Marc</option>+-- > <option value="2">Andreas</option>+-- > </select>+-- > </form>+--+-- If you want a certain value to be preselected, set the value in the controller. For example, to have the first user be preselected in the above example:+--+-- > action NewProjectAction = do+-- > users <- query @User |> fetch+-- > let userId = headMay users |> maybe def (.id)+-- > let target = newRecord @Project |> set #userId userId+-- > render NewView { .. }+selectField :: forall fieldName model item.+ ( ?formContext :: FormContext model+ , HasField fieldName model (SelectValue item)+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , KnownSymbol (GetModelName model)+ , CanSelect item+ , InputValue (SelectValue item)+ , Typeable model+ , Eq (SelectValue item)+ , FieldBit fieldName model+ ) => Proxy fieldName -> [item] -> FormField+selectField field items = FormField+ { fieldType =+ let+ itemToTuple :: item -> (Text, Text)+ itemToTuple item = (selectLabel item, inputValue (selectValue item))+ in+ SelectInput (map itemToTuple items)+ , fieldName = cs fieldName+ , fieldLabel = removeIdSuffix $ fieldNameToFieldLabel (cs fieldName)+ -- If the field is not touched, we don't want to render the value from the model+ -- so we force the user to select. If a value was explicitely set in the model, we+ -- render that value.+ , fieldValue = if IHP.ModelSupport.didTouchField field model || (not $ isNew model)+ then inputValue (getField @fieldName model :: SelectValue item)+ else ""+ , fieldInputId = cs (lcfirst (getModelName @model) <> "_" <> cs fieldName)+ , validatorResult = getValidationViolation field model+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = ?formContext.cssFramework+ , helpText = ""+ , placeholder = "Please select"+ , required = False+ , autofocus = False+ }+ where+ fieldName = symbolVal field+ FormContext { model } = ?formContext+{-# INLINE selectField #-}++-- | Radio require you to pass a list of possible values to select. We use the same mechanism as for for 'selectField'.+--+-- > formFor project [hsx|+-- > {radioField #userId users}+-- > |]+--+-- In the example above the variable users contains all the possible option values for the radios.+--+-- You also need to define a instance @CanSelect User@:+--+-- > instance CanSelect User where+-- > -- Here we specify that the <option> value should contain a `Id User`+-- > type SelectValue User = Id User+-- > -- Here we specify how to transform the model into <option>-value+-- > selectValue user = user.id+-- > -- And here we specify the <option>-text+-- > selectLabel user = user.name+--+-- Given the above example, the rendered form will look like this (omitting classes for brevity):+--+-- > <!-- Assuming: users = [User { id = 1, name = "Marc" }, User { id = 2, name = "Andreas" }] -->+-- > <form ...>+-- > <fieldset>+-- > <div>+-- > <input type="radio" id="option1" value="1"/>+-- > <label for="option1">Marc</label>+-- > </div>+-- > <div>+-- > <input type="radio" id="option2" value="2"/>+-- > <label for="option2">Andreas</label>+-- > </div>+-- > </fieldset>+-- > </form>+--+-- If you want a certain value to be preselected, set the value in the controller. For example, to have the first user be preselected in the above example:+--+-- > action NewProjectAction = do+-- > users <- query @User |> fetch+-- > let userId = headMay users |> maybe def (.id)+-- > let target = newRecord @Project |> set #userId userId+-- > render NewView { .. }+radioField :: forall fieldName model item.+ ( ?formContext :: FormContext model+ , HasField fieldName model (SelectValue item)+ , HasField "meta" model MetaBag+ , KnownSymbol fieldName+ , KnownSymbol (GetModelName model)+ , CanSelect item+ , InputValue (SelectValue item)+ , Typeable model+ , Eq (SelectValue item)+ , FieldBit fieldName model+ ) => Proxy fieldName -> [item] -> FormField+radioField field items = (selectField field items)+ { fieldType =+ let+ itemToTuple :: item -> (Text, Text)+ itemToTuple item = (selectLabel item, inputValue (selectValue item))+ in+ RadioInput (map itemToTuple items)+ , placeholder = ""+ }+{-# INLINE radioField #-}++class CanSelect model where+ -- | Here we specify the type of the @<option>@ value, usually an @Id model@+ type SelectValue model :: GHC.Types.Type++ -- | Here we specify the <option>-text+ selectLabel :: model -> Text+ default selectLabel :: Show model => model -> Text+ selectLabel = tshow++ -- | Here we specify how to transform the model into @<option>@-value+ selectValue :: model -> SelectValue model+ default selectValue :: HasField "id" model (SelectValue model) => model -> SelectValue model+ selectValue = (.id)
@@ -5,7 +5,11 @@ -} module IHP.View.TimeAgo (timeAgo, dateTime, date, time) where -import IHP.Prelude+import Prelude+import Data.Text (Text)+import Data.Time.Clock (UTCTime)+import Data.Time.Format (formatTime, defaultTimeLocale)+import Data.String.Conversions (cs) import Data.Time.Format.ISO8601 (iso8601Show) import IHP.HSX.ConvertibleStrings () import Text.Blaze.Html5 (Html, (!))
@@ -18,15 +18,19 @@ ) where -import IHP.Prelude hiding (div)+import Prelude+import Data.Text (Text)+import Data.ByteString (ByteString)+import IHP.HaskellSupport (SetField(..)) import qualified Text.Blaze.Html5 as Blaze-import IHP.FlashMessages.Types-import IHP.ModelSupport (Violation)+import Network.Wai.Middleware.FlashMessages (FlashMessage (..))+import IHP.ModelSupport.Types (Violation) import IHP.Breadcrumb.Types import IHP.Pagination.Types+import Network.Wai (Request) -type HtmlWithContext context = (?context :: context) => Blaze.Html+type HtmlWithContext context = (?context :: context, ?request :: Request) => Blaze.Html -- | A layout is just a function taking a view and returning a new view. --@@ -199,6 +203,8 @@ , styledInputInvalidClass :: CSSFramework -> FormField -> Text -- | Class applied to the div wrapping the label and input, e.g. @"form-group"@ , styledFormGroupClass :: Text+ -- | Class applied to @<label>@ elements in form fields, e.g. @"form-label"@+ , styledLabelClass :: Text -- | Elements that containers the validation error message for a invalid form field , styledValidationResult :: CSSFramework -> FormField -> Blaze.Html -- | Class name for container of validation error message
@@ -1,8 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--module IHP.ViewErrorMessages where----instance TypeError (Text "Cannot 'Show' functions." :$$: Text "Perhaps there is a missing argument?") => Show (a -> b) where--- showsPrec = error "unreachable"
@@ -1,3 +1,4 @@+{-# OPTIONS_HADDOCK not-home, hide #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-}@@ -25,7 +26,7 @@ module Data.Aeson, module IHP.AutoRefresh.View, module IHP.View.Classes,- module IHP.FlashMessages.ViewFunctions,+ module IHP.FlashMessages, module IHP.Controller.Context, module IHP.Controller.Layout, module IHP.Modal.Types,@@ -33,14 +34,13 @@ module IHP.Job.Types, module IHP.LoginSupport.Helper.View, module IHP.PageHead.ViewFunctions,- module IHP.Assets.ViewFunctions, module IHP.Breadcrumb.ViewFunctions, module IHP.Pagination.ViewFunctions,+ module Network.Wai, ) where import IHP.Prelude-import IHP.ViewErrorMessages ()-import IHP.ViewSupport+import IHP.ViewSupport import Text.Blaze (stringValue, (!)) import Text.Blaze.Html5 (preEscapedToHtml, preEscapedTextValue) import IHP.View.Form@@ -56,7 +56,7 @@ import IHP.AutoRefresh.View import IHP.View.Types import IHP.View.Classes-import IHP.FlashMessages.ViewFunctions+import IHP.FlashMessages import IHP.Controller.Context import IHP.Controller.Layout @@ -67,6 +67,10 @@ import IHP.LoginSupport.Helper.View import IHP.PageHead.ViewFunctions -import IHP.Assets.ViewFunctions import IHP.Breadcrumb.ViewFunctions import IHP.Pagination.ViewFunctions+import Network.Wai++instance ToHtml OsPath where+ {-# INLINE toHtml #-}+ toHtml = toHtml . osPathToText
@@ -18,8 +18,6 @@ , onClick , onLoad , theRequest-, viewContext-, addStyle , ViewFetchHelpMessage , param , fetch@@ -31,6 +29,8 @@ , theCSSFramework , fromCSSFramework , liveReloadWebsocketUrl+, assetPath+, assetVersion ) where import IHP.Prelude@@ -44,26 +44,26 @@ import qualified Data.Either as Either import GHC.TypeLits as T import qualified Data.ByteString as ByteString-import IHP.RouterSupport hiding (get)-import qualified Network.Wai as Wai+import IHP.Router.UrlGenerator (HasPath(..))+import Network.Wai import Text.Blaze.Html5.Attributes as A import IHP.HSX.QQ (hsx) import IHP.HSX.ToHtml import qualified Data.Sequences as Sequences-import qualified IHP.Controller.RequestContext import qualified IHP.View.CSSFramework as CSSFramework () import IHP.View.Types import qualified IHP.FrameworkConfig as FrameworkConfig-import IHP.Controller.Context import qualified IHP.HSX.Attribute as HSX+import qualified Network.Wai.Middleware.AssetPath as AssetPath+import IHP.ActionType (isActiveController) class View theView where -- | Hook which is called before the render is called- beforeRender :: (?context :: ControllerContext) => theView -> IO ()+ beforeRender :: (?context :: ControllerContext, ?request :: Request) => theView -> IO () beforeRender view = pure () -- Renders the view as html- html :: (?context :: ControllerContext, ?view :: theView) => theView -> Html5.Html+ html :: (?context :: ControllerContext, ?view :: theView, ?request :: Request) => theView -> Html5.Html -- | Renders the view to a JSON json :: theView -> JSON.Value@@ -115,10 +115,10 @@ -- False -- -- This function returns @False@ when a sub-path is request. Use 'isActivePathOrSub' if you want this example to return @True@.-isActivePath :: (?context :: ControllerContext, PathString controller) => controller -> Bool+isActivePath :: (?request :: Request, PathString controller) => controller -> Bool isActivePath route = let- currentPath = Wai.rawPathInfo theRequest <> Wai.rawQueryString theRequest+ currentPath = theRequest.rawPathInfo <> theRequest.rawQueryString in currentPath == cs (pathToString route) @@ -135,28 +135,13 @@ -- True -- -- Also see 'isActivePath'.-isActivePathOrSub :: (?context :: ControllerContext, PathString controller) => controller -> Bool+isActivePathOrSub :: (?request :: Request, PathString controller) => controller -> Bool isActivePathOrSub route = let- currentPath = Wai.rawPathInfo theRequest+ currentPath = theRequest.rawPathInfo in cs (pathToString route) `ByteString.isPrefixOf` currentPath --- | Returns @True@ when the given type matches the type of the currently executed controller action------ __Example:__ The browser has requested @\/Posts@ and the @Posts@ action of the @PostsController@ is called.------ >>> isActiveController @PostsController--- True------ Returns @True@ because the current action is part of the @PostsController@-isActiveController :: forall controller. (?context :: ControllerContext, Typeable controller) => Bool-isActiveController =- let- (ActionType actionType) = fromFrozenContext @ActionType- in- (Typeable.typeRep @Proxy @controller (Proxy @controller)) == actionType- -- | Returns @True@ when the given action matches the path of the currently executed action -- -- __Example:__ The browser has requested @\/PostsAction@.@@ -172,7 +157,7 @@ -- >>> isActiveAction (ShowPostAction postId) -- True ---isActiveAction :: forall controllerAction. (?context::ControllerContext, HasPath controllerAction) => controllerAction -> Bool+isActiveAction :: forall controllerAction. (?request :: Request, HasPath controllerAction) => controllerAction -> Bool isActiveAction controllerAction = isActivePath (pathTo controllerAction) @@ -182,8 +167,8 @@ onLoad = A.onload -- | Returns the current request-theRequest :: (?context :: ControllerContext) => Wai.Request-theRequest = ?context.requestContext.request+theRequest :: (?request :: Request) => Request+theRequest = ?request {-# INLINE theRequest #-} class PathString a where@@ -195,30 +180,6 @@ instance {-# OVERLAPPABLE #-} HasPath action => PathString action where pathToString = pathTo --- | Alias for @?context@-viewContext :: (?context :: ControllerContext) => ControllerContext-viewContext = ?context-{-# INLINE viewContext #-}---- | Adds an inline style element to the html.------ This helps to work around the issue, that our HSX parser cannot deal with CSS yet.------ __Example:__------ > myStyle = addStyle "#my-div { color: blue; }"--- > [hsx|{myStyle}<div id="my-div">Hello World</div>|]------ This will render like:------ > <style>--- > #my-div { color: blue; }--- > </style>--- > <div id="my-div">Hello World</div>-addStyle :: (ConvertibleStrings string Text) => string -> Html5.Markup-addStyle style = Html5.style (Html5.preEscapedText (cs style))-{-# INLINE addStyle #-}- -- | This class provides helpful compile-time error messages when you use common -- controller functions inside of your views. class ViewParamHelpMessage where@@ -239,11 +200,11 @@ instance (T.TypeError (T.Text "Looks like you forgot to pass a " :<>: (T.ShowType (GetModelByTableName record)) :<>: T.Text " id to this data constructor.")) => Eq (Id' (record :: T.Symbol) -> controller) where a == b = error "unreachable" -fromCSSFramework :: (?context :: ControllerContext, KnownSymbol field, HasField field CSSFramework (CSSFramework -> appliedFunction)) => Proxy field -> appliedFunction+fromCSSFramework :: (?request :: Request, KnownSymbol field, HasField field CSSFramework (CSSFramework -> appliedFunction)) => Proxy field -> appliedFunction fromCSSFramework field = let cssFramework = theCSSFramework in (get field cssFramework) cssFramework -theCSSFramework :: (?context :: ControllerContext) => CSSFramework-theCSSFramework = ?context.frameworkConfig.cssFramework+theCSSFramework :: (?request :: Request) => CSSFramework+theCSSFramework = ?request.frameworkConfig.cssFramework -- | Replaces all newline characters with a @<br>@ tag. Useful for displaying preformatted text. --@@ -258,10 +219,31 @@ type Html = HtmlWithContext ControllerContext -- | The URL for the dev-mode live reload server. Typically "ws://localhost:8001"-liveReloadWebsocketUrl :: (?context :: ControllerContext) => Text-liveReloadWebsocketUrl = ?context.frameworkConfig.ideBaseUrl+liveReloadWebsocketUrl :: (?request :: Request) => Text+liveReloadWebsocketUrl = ?request.frameworkConfig.ideBaseUrl |> Text.replace "http://" "ws://" |> Text.replace "https://" "wss://" instance InputValue (PrimaryKey table) => HSX.ApplyAttribute (Id' table) where applyAttribute attr attr' value h = HSX.applyAttribute attr attr' (inputValue value) h+++-- | Adds a cache buster to a asset path+--+-- >>> assetPath "/keyhandlers.js"+-- "/keyhandlers.js?v=9be8995c-7055-43d9-a1b2-43e05c210271"+--+-- The asset version can be configured using the+-- @IHP_ASSET_VERSION@ environment variable.+assetPath :: (?request :: Request) => Text -> Text+assetPath assetPath = AssetPath.assetPath theRequest assetPath++-- | Returns the assetVersion+--+-- >>> assetVersion+-- "9be8995c-7055-43d9-a1b2-43e05c210271"+--+-- The asset version can be configured using the+-- @IHP_ASSET_VERSION@ environment variable.+assetVersion :: (?request :: Request) => Text+assetVersion = fromMaybe (error "assetPath middleware missing") (AssetPath.assetVersion theRequest)
@@ -19,13 +19,12 @@ import IHP.Prelude import qualified Network.WebSockets as Websocket import Network.WebSockets.Connection.PingPong (withPingPong, defaultPingPongOptions)-import IHP.ApplicationContext-import IHP.Controller.RequestContext import qualified Data.UUID as UUID import qualified Data.Maybe as Maybe import qualified Control.Exception.Safe as Exception import IHP.Controller.Context import qualified Data.Aeson as Aeson+import Network.Wai (Request) import qualified IHP.Log as Log @@ -34,13 +33,13 @@ class WSApp state where initialState :: state - run :: (?state :: IORef state, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection) => IO ()+ run :: (?state :: IORef state, ?context :: ControllerContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection, ?request :: Request) => IO () run = pure () - onPing :: (?state :: IORef state, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection) => IO ()+ onPing :: (?state :: IORef state, ?context :: ControllerContext, ?modelContext :: ModelContext, ?request :: Request) => IO () onPing = pure () - onClose :: (?state :: IORef state, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection) => IO ()+ onClose :: (?state :: IORef state, ?context :: ControllerContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection, ?request :: Request) => IO () onClose = pure () -- | Provide WebSocket Connection Options@@ -60,12 +59,13 @@ connectionOptions :: WebSocket.ConnectionOptions connectionOptions = WebSocket.defaultConnectionOptions -startWSApp :: forall state. (WSApp state, ?applicationContext :: ApplicationContext, ?requestContext :: RequestContext, ?context :: ControllerContext, ?modelContext :: ModelContext) => state -> Websocket.Connection -> IO ()+startWSApp :: forall state. (WSApp state, ?context :: ControllerContext, ?modelContext :: ModelContext) => state -> Websocket.Connection -> IO () startWSApp initialState connection = do state <- newIORef initialState let ?state = state+ let ?request = ?context.request - result <- Exception.try ((withPingPong defaultPingPongOptions connection (\connection -> let ?connection = connection in run @state)) `Exception.finally` (let ?connection = connection in onClose @state))+ result <- Exception.try ((withPingPong (defaultPingPongOptions { Websocket.pingAction = onPing @state }) connection (\connection -> let ?connection = connection in run @state)) `Exception.finally` (let ?connection = connection in onClose @state)) case result of Left (e@Exception.SomeException{}) -> case Exception.fromException e of
@@ -1,65 +0,0 @@-{-# LANGUAGE UndecidableInstances #-}-module IHP.Welcome.Controller where--import IHP.ControllerPrelude-import IHP.RouterPrelude-import IHP.ViewPrelude--data WelcomeController = WelcomeAction deriving (Eq, Show, Data)--instance CanRoute WelcomeController where- parseRoute' = (string "/" <|> string "") *> endOfInput $> WelcomeAction--instance HasPath WelcomeController where- pathTo WelcomeAction = "/"--instance Controller WelcomeController where- action WelcomeAction = respondHtml (renderLayout view)--view :: Html-view = [hsx|-<div style="background-color: #657b83; padding: 2rem; color:hsla(196, 13%, 96%, 1)">- <div style="max-width: 800px; margin-left: auto; margin-right: auto">- <h1 style="margin-bottom: 2rem; font-size: 2rem; font-weight: 300; border-bottom: 1px solid white; padding-bottom: 0.25rem; border-color: hsla(196, 13%, 60%, 1)">λ IHP</h1>- - <h2 style="margin-top: 0; margin-bottom: 0rem; font-weight: 900; font-size: 3rem">It's working!</h2>- <p style="margin-top: 1rem; font-size: 1.75rem; font-weight: 600; color:hsla(196, 13%, 80%, 1)">Your new application is up and running.</p>-- <a- href="https://ihp.digitallyinduced.com/Guide/your-first-project.html"- style="margin-top: 2rem; background-color: #268bd2; padding: 1rem; border-radius: 3px; color: hsla(205, 69%, 98%, 1); text-decoration: none; font-weight: bold; display: inline-block; box-shadow: 0 4px 6px hsla(205, 69%, 0%, 0.08); transition: box-shadow 0.2s; transition: transform 0.2s;"- target="_blank"- >Learn the Next Steps in the Documentation</a>- </div>-</div>--<div style="max-width: 800px; margin-left: auto; margin-right: auto; margin-top: 4rem">- {icon}-- <p style="color: hsla(196, 13%, 50%, 1); margin-top: 4rem">- You can change this start page by updating 'startPage WelcomeAction' in 'Web/FrontController.hs'.- </p>-</div>-|]--renderLayout :: Layout-renderLayout view = [hsx|-<!DOCTYPE html>-<html lang="en">- <head>- <meta charset="utf-8"/>- <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>- <title>Welcome to IHP</title>-- <script id="livereload-script" src="/livereload.js"></script>- </head>- <body style={bodyStyle}>{view}</body>-</html>-|]- where- bodyStyle :: Text- bodyStyle = "margin: 0; font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Helvetica Neue\", Arial, sans-serif;"--icon = preEscapedToHtml [plain|-<svg id="ab0c8d57-73b5-4efc-a896-a2305a232bd8" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="1027.4958" height="696.05876" viewBox="0 0 1027.4958 696.05876"><title>experience design</title><circle cx="696.2479" cy="355.70168" r="76.7521" fill="#657b83" opacity="0.4"/><rect x="270" y="461.90193" width="428" height="116" fill="#3f3d56"/><path d="M756.83869,276.47654l-17.08989,8.08834,15.1253-11.3099a85.35244,85.35244,0,0,0-8.25839-10.86725L672.499,280.80982l66.94832-25.53848a85.33888,85.33888,0,0,0-9.72181-7.41015L616.789,266.0523a84.63924,84.63924,0,0,0-18.53688,52.82025h170A84.59367,84.59367,0,0,0,756.83869,276.47654Z" transform="translate(-86.2521 -101.97062)" fill="#f2f2f2"/><path d="M770.37527,152.166A495.97179,495.97179,0,0,0,552.2521,101.97062c-153.82965,0-291.36139,69.75269-382.71033,179.34869C325.96817,247.04484,598.319,187.71378,770.37527,152.166Z" transform="translate(-86.2521 -101.97062)" fill="#f2f2f2"/><rect y="693.90193" width="1020" height="2" fill="#2f2e41"/><path d="M682.11649,318.87255,624.2521,232.92193s87.63109,30.59259,77.68592,85.95062Z" transform="translate(-86.2521 -101.97062)" fill="#3f3d56"/><path d="M684.38771,318.87255l57.86439-85.95062s-87.63109,30.59259-77.68591,85.95062Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><path d="M784.7521,797.87255h-429v-640h429Zm-427-2h425v-636h-425Z" transform="translate(-86.2521 -101.97062)" fill="#2f2e41"/><rect x="271" y="71.08308" width="427" height="2" fill="#2f2e41"/><circle cx="282" cy="63.90193" r="4" fill="#657b83"/><circle cx="293" cy="63.90193" r="4" fill="#657b83"/><circle cx="304" cy="63.90193" r="4" fill="#657b83"/><rect x="286" y="84.90193" width="18" height="10" fill="#657b83"/><rect x="286" y="163.90193" width="88" height="10" fill="#657b83"/><rect x="685.2521" y="186.87255" width="88" height="10" transform="translate(1372.2521 281.77448) rotate(-180)" fill="#f2f2f2"/><rect x="286" y="180.90193" width="148" height="6" fill="#f2f2f2"/><rect x="286" y="193.90193" width="140" height="6" fill="#f2f2f2"/><rect x="286" y="206.90193" width="102" height="6" fill="#f2f2f2"/><rect x="320" y="384.90193" width="58" height="6" fill="#f2f2f2"/><rect x="320" y="397.90193" width="54.86486" height="6" fill="#f2f2f2"/><rect x="320" y="410.90193" width="39.97297" height="6" fill="#f2f2f2"/><rect x="455" y="384.90193" width="58" height="6" fill="#f2f2f2"/><rect x="455" y="397.90193" width="54.86486" height="6" fill="#f2f2f2"/><rect x="455" y="410.90193" width="39.97297" height="6" fill="#f2f2f2"/><rect x="595" y="384.90193" width="58" height="6" fill="#f2f2f2"/><rect x="595" y="397.90193" width="54.86486" height="6" fill="#f2f2f2"/><rect x="595" y="410.90193" width="39.97297" height="6" fill="#f2f2f2"/><rect x="410" y="611.90193" width="88" height="10" fill="#657b83"/><rect x="410" y="628.90193" width="148" height="6" fill="#f2f2f2"/><rect x="410" y="641.90193" width="140" height="6" fill="#f2f2f2"/><rect x="410" y="654.90193" width="102" height="6" fill="#f2f2f2"/><rect x="312.5" y="271.90193" width="348" height="2" fill="#657b83" opacity="0.3"/><rect x="312" y="306.90193" width="74" height="66" fill="#657b83" opacity="0.3"/><rect x="447" y="306.90193" width="74" height="66" fill="#3f3d56"/><rect x="587" y="306.90193" width="74" height="66" fill="#3f3d56"/><path d="M560.61073,474.59512l-.97627-.01907c.021-1.08114.63114-26.56133,10.66219-33.604l.56059.79893C561.23091,448.52955,560.6155,474.33532,560.61073,474.59512Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><circle cx="489.20618" cy="333.79786" r="4.88134" fill="#657b83"/><path d="M574.47138,453.00423a15.19766,15.19766,0,0,1-7.84149.33859,13.85019,13.85019,0,0,1,14.23985-4.88441A15.19761,15.19761,0,0,1,574.47138,453.00423Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><path d="M700.61073,474.59512l-.97627-.01907c.021-1.08114.63114-26.56133,10.66219-33.604l.56059.79893C701.23091,448.52955,700.6155,474.33532,700.61073,474.59512Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><circle cx="629.20618" cy="333.79786" r="4.88134" fill="#657b83"/><path d="M714.47138,453.00423a15.19766,15.19766,0,0,1-7.84149.33859,13.85019,13.85019,0,0,1,14.23985-4.88441A15.19761,15.19761,0,0,1,714.47138,453.00423Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><rect x="286" y="498.90193" width="88" height="10" fill="#657b83"/><rect x="286" y="515.90193" width="148" height="6" fill="#f2f2f2"/><rect x="286" y="528.90193" width="140" height="6" fill="#f2f2f2"/><rect x="286" y="541.90193" width="102" height="6" fill="#f2f2f2"/><circle cx="611.7521" cy="512.70168" r="31.2479" fill="#657b83" opacity="0.4"/><circle cx="553.92017" cy="527.62605" r="21.92017" fill="#657b83" opacity="0.4"/><circle cx="167.5" cy="582.40193" r="79" fill="#2f2e41"/><rect x="131.5" y="644.40193" width="24" height="43" fill="#2f2e41"/><rect x="179.5" y="644.40193" width="24" height="43" fill="#2f2e41"/><ellipse cx="151.5" cy="687.90193" rx="20" ry="7.5" fill="#2f2e41"/><ellipse cx="199.5" cy="686.90193" rx="20" ry="7.5" fill="#2f2e41"/><circle cx="169.5" cy="562.40193" r="27" fill="#fff"/><circle cx="169.5" cy="550.40193" r="9" fill="#3f3d56"/><path d="M177.11943,610.90483c-6.37889-28.56758,14.01185-57.43392,45.544-64.47477s62.2651,10.41,68.644,38.9776-14.51861,39.10379-46.05075,46.14464S183.49832,639.4724,177.11943,610.90483Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><ellipse cx="313.52848" cy="596.95692" rx="39.5" ry="12.40027" transform="translate(-446.20984 527.75787) rotate(-65.07683)" fill="#2f2e41"/><ellipse cx="168.52848" cy="620.95692" rx="12.40027" ry="39.5" transform="translate(-357.17251 49.01394) rotate(-27.89329)" fill="#2f2e41"/><ellipse cx="906.25518" cy="625.62283" rx="12.40027" ry="39.5" transform="translate(-250.61562 265.81898) rotate(-21.07832)" fill="#2f2e41"/><circle cx="893.18955" cy="583.10381" r="79" fill="#2f2e41"/><rect x="991.67544" y="747.00687" width="24" height="43" transform="matrix(-1, 0.0028, -0.0028, -1, 1923.24743, 1432.22882)" fill="#2f2e41"/><rect x="943.67563" y="747.14132" width="24" height="43" transform="translate(1827.24837 1432.63217) rotate(179.83951)" fill="#2f2e41"/><ellipse cx="995.7371" cy="790.5292" rx="20" ry="7.5" transform="translate(-88.46246 -99.17847) rotate(-0.16049)" fill="#2f2e41"/><ellipse cx="947.73448" cy="789.66365" rx="20" ry="7.5" transform="translate(-88.46023 -99.31292) rotate(-0.16049)" fill="#2f2e41"/><circle cx="891.13354" cy="563.10949" r="27" fill="#fff"/><circle cx="891.13354" cy="563.10949" r="9" fill="#3f3d56"/><path d="M1055.86824,611.39234c6.29884-28.58533-14.17267-57.39444-45.72441-64.34694s-62.23569,10.58438-68.53454,39.16971,14.62808,39.063,46.17982,46.01548S1049.56939,639.97768,1055.86824,611.39234Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><ellipse cx="1068.31896" cy="648.16898" rx="39.5" ry="12.40027" transform="translate(-261.30169 421.84772) rotate(-25.53475)" fill="#2f2e41"/><path d="M1018.32588,713.46562c.02166,7.732-19.84411,23.05568-41.93541,23.11755s-42.86536-21.648-42.887-29.38,20.71728,1.442,42.80858,1.3801S1018.30423,705.73367,1018.32588,713.46562Z" transform="translate(-86.2521 -101.97062)" fill="#fff"/><circle cx="917.2479" cy="468.70168" r="110.2479" fill="#657b83" opacity="0.4"/><circle cx="190.40483" cy="348.59022" r="79" fill="#2f2e41"/><rect x="226.16663" y="506.95016" width="24" height="43" transform="translate(11.69174 -135.94422) rotate(10.26106)" fill="#2f2e41"/><rect x="273.39893" y="515.50057" width="24" height="43" transform="translate(13.97028 -144.22112) rotate(10.26106)" fill="#2f2e41"/><ellipse cx="285.68749" cy="563.4754" rx="7.5" ry="20" transform="translate(-415.18867 315.35713) rotate(-49.73894)" fill="#2f2e41"/><ellipse cx="239.4392" cy="555.10313" rx="7.5" ry="20" transform="translate(-425.15907 277.10318) rotate(-49.73894)" fill="#2f2e41"/><circle cx="195.93551" cy="329.26636" r="27" fill="#fff"/><circle cx="207.03243" cy="319.5883" r="9" fill="#3f3d56"/><path d="M214.337,364.61728c-1.188-29.247,24.01865-54.01935,56.30068-55.33066s59.41489,21.335,60.60291,50.582-21.2521,35.89214-53.53414,37.20345S215.525,393.86426,214.337,364.61728Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><ellipse cx="349.6025" cy="436.16834" rx="39.5" ry="12.40027" transform="translate(-174.86437 -12.8362) rotate(-12.9101)" fill="#2f2e41"/><ellipse cx="194.52848" cy="461.95692" rx="39.5" ry="12.40027" transform="translate(-184.5461 -46.83125) rotate(-12.9101)" fill="#2f2e41"/><path d="M254.95954,481.69373a18,18,0,0,0,35.42423,6.41281c1.77085-9.78213-5.79238-13.24363-15.57451-15.01448S256.73039,471.9116,254.95954,481.69373Z" transform="translate(-86.2521 -101.97062)" fill="#fff"/><rect x="380.7521" y="374.37255" width="74" height="66" transform="translate(48.33035 -204.77859) rotate(16.68876)" fill="#3f3d56"/><path d="M399.11981,435.94807l-.92967-.29863c.33056-1.02957,8.23224-25.26129,19.86323-29.12666l.30755.92627C407.19918,411.15851,399.199,435.70058,399.11981,435.94807Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/><circle cx="338.23982" cy="301.05004" r="4.88134" fill="#657b83"/><path d="M418.59694,419.247a15.19762,15.19762,0,0,1-7.60842-1.92753,13.85017,13.85017,0,0,1,15.04271-.58938A15.19758,15.19758,0,0,1,418.59694,419.247Z" transform="translate(-86.2521 -101.97062)" fill="#657b83"/></svg>-|]
@@ -0,0 +1,129 @@+{-# LANGUAGE AllowAmbiguousTypes, OverloadedStrings, BangPatterns, ScopedTypeVariables, TypeFamilies, DeriveDataTypeable #-}+module Main where++import Prelude hiding (take)+import Data.ByteString (ByteString)+import Data.Data+import Network.Wai (defaultRequest, Request(..))+import IHP.RouterSupport hiding (get)+import IHP.ControllerSupport+import Test.Tasty.Bench++-- Application type for benchmarking+data BenchApp = BenchApp deriving (Eq, Show, Data)++-- 5 controllers with 15 actions each = 75 total routes+-- All in module Main, so prefix is "/main/"+-- Action names follow pattern: <ControllerPrefix><Letter>Action++data BenchController1+ = Ctrl1AlphaAction | Ctrl1BravoAction | Ctrl1CharlieAction | Ctrl1DeltaAction | Ctrl1EchoAction+ | Ctrl1FoxtrotAction | Ctrl1GolfAction | Ctrl1HotelAction | Ctrl1IndiaAction | Ctrl1JulietAction+ | Ctrl1KiloAction | Ctrl1LimaAction | Ctrl1MikeAction | Ctrl1NovemberAction | Ctrl1OscarAction+ deriving (Eq, Show, Data)++data BenchController2+ = Ctrl2AlphaAction | Ctrl2BravoAction | Ctrl2CharlieAction | Ctrl2DeltaAction | Ctrl2EchoAction+ | Ctrl2FoxtrotAction | Ctrl2GolfAction | Ctrl2HotelAction | Ctrl2IndiaAction | Ctrl2JulietAction+ | Ctrl2KiloAction | Ctrl2LimaAction | Ctrl2MikeAction | Ctrl2NovemberAction | Ctrl2OscarAction+ deriving (Eq, Show, Data)++data BenchController3+ = Ctrl3AlphaAction | Ctrl3BravoAction | Ctrl3CharlieAction | Ctrl3DeltaAction | Ctrl3EchoAction+ | Ctrl3FoxtrotAction | Ctrl3GolfAction | Ctrl3HotelAction | Ctrl3IndiaAction | Ctrl3JulietAction+ | Ctrl3KiloAction | Ctrl3LimaAction | Ctrl3MikeAction | Ctrl3NovemberAction | Ctrl3OscarAction+ deriving (Eq, Show, Data)++data BenchController4+ = Ctrl4AlphaAction | Ctrl4BravoAction | Ctrl4CharlieAction | Ctrl4DeltaAction | Ctrl4EchoAction+ | Ctrl4FoxtrotAction | Ctrl4GolfAction | Ctrl4HotelAction | Ctrl4IndiaAction | Ctrl4JulietAction+ | Ctrl4KiloAction | Ctrl4LimaAction | Ctrl4MikeAction | Ctrl4NovemberAction | Ctrl4OscarAction+ deriving (Eq, Show, Data)++data BenchController5+ = Ctrl5AlphaAction | Ctrl5BravoAction | Ctrl5CharlieAction | Ctrl5DeltaAction | Ctrl5EchoAction+ | Ctrl5FoxtrotAction | Ctrl5GolfAction | Ctrl5HotelAction | Ctrl5IndiaAction | Ctrl5JulietAction+ | Ctrl5KiloAction | Ctrl5LimaAction | Ctrl5MikeAction | Ctrl5NovemberAction | Ctrl5OscarAction+ deriving (Eq, Show, Data)++-- AutoRoute instances (all defaults)+instance AutoRoute BenchController1+instance AutoRoute BenchController2+instance AutoRoute BenchController3+instance AutoRoute BenchController4+instance AutoRoute BenchController5++-- Controller instances (action never called during routing benchmark)+instance Controller BenchController1 where action _ = error "not called"+instance Controller BenchController2 where action _ = error "not called"+instance Controller BenchController3 where action _ = error "not called"+instance Controller BenchController4 where action _ = error "not called"+instance Controller BenchController5 where action _ = error "not called"++instance InitControllerContext BenchApp where+ initContext = pure ()++instance FrontController BenchApp where+ controllers =+ [ parseRoute @BenchController1+ , parseRoute @BenchController2+ , parseRoute @BenchController3+ , parseRoute @BenchController4+ , parseRoute @BenchController5+ ]++-- Mock request for benchmarking+mockRequest :: ByteString -> Request+mockRequest path = defaultRequest+ { rawPathInfo = path+ , requestMethod = "GET"+ , queryString = []+ }++-- Benchmark direct dispatch (like frontControllerToWAIApp does: findInRouteMaps)+benchDirectDispatch :: ByteString -> ()+benchDirectDispatch !path =+ let ?request = mockRequest path+ ?respond = error "not used"+ ?application = BenchApp+ in let allRoutes = controllers @BenchApp+ in case findInRouteMaps path allRoutes of+ Just _ -> ()+ Nothing -> ()++-- Benchmark single controller direct dispatch+data SingleControllerApp = SingleControllerApp deriving (Eq, Show, Data)++instance InitControllerContext SingleControllerApp where+ initContext = pure ()++instance FrontController SingleControllerApp where+ controllers = [ parseRoute @BenchController1 ]++benchSingleDirect :: ByteString -> ()+benchSingleDirect !path =+ let ?request = mockRequest path+ ?respond = error "not used"+ ?application = SingleControllerApp+ in let allRoutes = controllers @SingleControllerApp+ in case findInRouteMaps path allRoutes of+ Just _ -> ()+ Nothing -> ()++main :: IO ()+main = defaultMain+ [ bgroup "single controller (15 actions)"+ [ bench "first action" $ whnf benchSingleDirect "/main/Ctrl1Alpha"+ , bench "last action" $ whnf benchSingleDirect "/main/Ctrl1Oscar"+ , bench "miss (wrong name)" $ whnf benchSingleDirect "/main/NoSuchAction"+ , bench "miss (wrong prefix)" $ whnf benchSingleDirect "/other/Ctrl1Alpha"+ ]+ , bgroup "5 controllers x 15 actions (75 routes)"+ [ bench "match controller 1, first action" $ whnf benchDirectDispatch "/main/Ctrl1Alpha"+ , bench "match controller 1, last action" $ whnf benchDirectDispatch "/main/Ctrl1Oscar"+ , bench "match controller 3, middle" $ whnf benchDirectDispatch "/main/Ctrl3Golf"+ , bench "match controller 5, last action" $ whnf benchDirectDispatch "/main/Ctrl5Oscar"+ , bench "miss (no match)" $ whnf benchDirectDispatch "/main/NoSuchAction"+ , bench "miss (wrong prefix)" $ whnf benchDirectDispatch "/other/Ctrl1Alpha"+ ]+ ]
@@ -0,0 +1,188 @@+{-|+Module: Test.AutoRefreshSpec+Tests that AutoRefresh preserves query parameters when re-rendering+with a bare WebSocket request (no query params).+-}+{-# LANGUAGE AllowAmbiguousTypes #-}++module Test.AutoRefreshSpec where+import Test.Hspec+import IHP.Prelude+import IHP.Environment+import IHP.FrameworkConfig+import IHP.ControllerPrelude hiding (get, request)+import Network.Wai+import Network.Wai.Internal (ResponseReceived(..))+import Network.HTTP.Types+import IHP.AutoRefresh (globalAutoRefreshServerVar, sessionResponseHasChanged, updateSession)+import IHP.AutoRefresh.Types+import qualified Control.Concurrent.MVar as MVar+import IHP.Controller.Response (ResponseException(..))+import qualified Control.Exception as Exception+import qualified IHP.PGListener as PGListener+import IHP.Log.Types (Logger(..), LogLevel(..))+import IHP.Server (initMiddlewareStack)+import IHP.Test.Mocking+import qualified Data.UUID as UUID+import qualified Network.Wai as Wai++data WebApplication = WebApplication deriving (Eq, Show, Data)++data TestController+ = ShowItemAction+ deriving (Eq, Show, Data)++instance Controller TestController where+ action ShowItemAction = autoRefresh do+ let marketId = param @Text "marketId"+ renderPlain (cs marketId)++instance AutoRoute TestController++instance FrontController WebApplication where+ controllers = [ parseRoute @TestController ]++instance InitControllerContext WebApplication where+ initContext = pure ()++instance FrontController RootApplication where+ controllers = [ mountFrontController WebApplication ]++config :: ConfigBuilder+config = do+ option Development+ option (AppPort 8000)++-- | Helper that calls a controller action with query parameters (GET-style)+-- and passes a PGListener to the middleware stack so autoRefresh can work.+callActionWithQueryParams+ :: forall application controller+ . ( Controller controller+ , ContextParameters application+ , Typeable application+ , Typeable controller+ )+ => PGListener.PGListener+ -> controller+ -> [(ByteString, ByteString)]+ -> IO Response+callActionWithQueryParams pgListener controller queryParams = do+ let MockContext { frameworkConfig, modelContext } = ?mocking++ -- Build request with query params (GET-style, not POST body)+ let baseRequest = ?request+ { Wai.queryString = map (\(k,v) -> (k, Just v)) queryParams+ , Wai.rawQueryString = renderSimpleQuery True queryParams+ }++ -- Capture the response+ responseRef <- newIORef Nothing+ let captureRespond response = do+ writeIORef responseRef (Just response)+ pure ResponseReceived++ -- Create the controller app+ let controllerApp req respond = do+ let ?request = req+ let ?respond = respond+ runActionWithNewContext controller++ -- Run through middleware stack with PGListener enabled+ middlewareStack <- initMiddlewareStack frameworkConfig modelContext (Just pgListener)+ _ <- middlewareStack controllerApp baseRequest captureRespond++ readIORef responseRef >>= \case+ Just response -> pure response+ Nothing -> error "callActionWithQueryParams: No response was returned by the controller"++testLogger :: Logger+testLogger = Logger+ { write = \_ -> pure ()+ , level = Debug+ , formatter = \_ _ msg -> msg+ , timeCache = pure ""+ , cleanup = pure ()+ }++tests :: Spec+tests = beforeAll (mockContextNoDatabase WebApplication config) do+ describe "AutoRefresh" do+ describe "renderView" do+ it "should preserve query parameters when re-rendering with a websocket request" $ withContext do+ -- Clean up any leftover global state from previous tests+ MVar.modifyMVar_ globalAutoRefreshServerVar (\_ -> pure Nothing)++ PGListener.withPGListener "" testLogger \pgListener -> do+ -- 1. Call the action with query params — this triggers autoRefresh+ -- which stores a session with renderView+ response <- callActionWithQueryParams pgListener ShowItemAction [("marketId", "abc-123")]+ body <- responseBody response+ cs body `shouldBe` ("abc-123" :: Text)++ -- 2. Extract the stored renderView from the AutoRefreshSession+ maybeServerRef <- MVar.readMVar globalAutoRefreshServerVar+ serverRef <- case maybeServerRef of+ Just ref -> pure ref+ Nothing -> error "AutoRefreshServer was not created"++ server <- readIORef serverRef+ session <- case server.sessions of+ (s:_) -> pure s+ [] -> error "No AutoRefresh sessions found"++ -- 3. Call renderView with a bare request (simulating WebSocket re-render)+ -- The WebSocket request has NO query params — this is the bug scenario+ let bareRequest = defaultRequest+ result <- Exception.try $ session.renderView bareRequest (\_ -> error "respond should not be called")+ case result of+ Left (ResponseException reResponse) -> do+ reBody <- responseBody reResponse+ -- If query params are NOT preserved, this would throw ParamNotFoundException+ -- instead of reaching here with the correct value+ cs reBody `shouldBe` ("abc-123" :: Text)+ Right _ ->+ expectationFailure "renderView should have thrown ResponseException"++ -- Cleanup+ MVar.modifyMVar_ globalAutoRefreshServerVar (\_ -> pure Nothing)++ describe "graceful degradation without PGListener" do+ it "should run the action without crashing when PGListener is not available" $ withContext do+ MVar.modifyMVar_ globalAutoRefreshServerVar (\_ -> pure Nothing)++ response <- callActionWithParams ShowItemAction [("marketId", "degraded-ok")]+ body <- responseBody response+ cs body `shouldBe` ("degraded-ok" :: Text)++ -- Verify autoRefresh skipped subscription machinery entirely+ maybeServerRef <- MVar.readMVar globalAutoRefreshServerVar+ case maybeServerRef of+ Nothing -> pure ()+ Just _ -> expectationFailure "Expected globalAutoRefreshServerVar to be Nothing"++ describe "session state tracking" do+ it "should compare re-rendered html against the latest session response" $ withContext do+ event <- MVar.newEmptyMVar+ now <- getCurrentTime+ let session =+ AutoRefreshSession+ { id = UUID.nil+ , renderView = \_ _ -> pure ()+ , event+ , tables = mempty+ , lastResponse = "resolved"+ , lastPing = now+ }+ serverRef <-+ newIORef+ AutoRefreshServer+ { subscriptions = []+ , sessions = [session]+ , subscribedTables = mempty+ , pgListener = error "pgListener unused in session state test"+ }++ updateSession serverRef UUID.nil (\currentSession -> currentSession { lastResponse = "unresolved" })++ sessionResponseHasChanged serverRef UUID.nil "resolved" `shouldReturn` True+ sessionResponseHasChanged serverRef UUID.nil "unresolved" `shouldReturn` False
@@ -0,0 +1,70 @@+{-|+Module: Test.Controller.AccessDeniedSpec+Tests for Access denied functions.+-}+{-# LANGUAGE AllowAmbiguousTypes #-}++module Test.Controller.AccessDeniedSpec where+import ClassyPrelude+import Test.Hspec+import IHP.Test.Mocking+import IHP.Prelude+import IHP.Environment+import IHP.RouterSupport hiding (get)+import IHP.FrameworkConfig+import IHP.ViewPrelude+import IHP.ControllerPrelude hiding (get, request)+import Network.Wai.Test+import Network.HTTP.Types++data WebApplication = WebApplication deriving (Eq, Show, Data)++data TestController+ = TestActionAccessDeniedWhen+ | TestActionAccessDeniedUnless+ deriving (Eq, Show, Data)++instance Controller TestController where+ action TestActionAccessDeniedWhen = do+ accessDeniedWhen True+ renderPlain "Test"+ action TestActionAccessDeniedUnless = do+ accessDeniedUnless False+ renderPlain "Test"++instance AutoRoute TestController++instance FrontController WebApplication where+ controllers = [ parseRoute @TestController ]+++defaultLayout :: Html -> Html+defaultLayout inner = [hsx|{inner}|]++instance InitControllerContext WebApplication where+ initContext = do+ setLayout defaultLayout++instance FrontController RootApplication where+ controllers = [ mountFrontController WebApplication ]++testGet :: ByteString -> Session SResponse+testGet url = request $ setPath defaultRequest { requestMethod = methodGet } url+++config = do+ option Development+ option (AppPort 8000)++assertAccessDenied :: SResponse -> IO ()+assertAccessDenied response = do+ response.simpleStatus `shouldBe` status403+ response.simpleBody `shouldNotBe` "Test"++tests :: Spec+tests = aroundAll (withMockContextAndApp WebApplication config) do+ describe "Access denied" $ do+ it "should return show 403 page when acessDeniedWhen is True" $ withContextAndApp \application -> do+ runSession (testGet "test/TestActionAccessDeniedWhen") application >>= assertAccessDenied+ it "should return show 403 page when acessDeniedUnless is False" $ withContextAndApp \application -> do+ runSession (testGet "test/TestActionAccessDeniedUnless") application >>= assertAccessDenied
@@ -0,0 +1,112 @@+{-|+Module: Test.Controller.ContextSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.Controller.ContextSpec where++import Test.Hspec+import IHP.Prelude+import IHP.Controller.Context+import Wai.Request.Params.Middleware (RequestBody (..), Respond)+import Control.Exception+import Network.Wai.Internal (ResponseReceived(..))+import Network.Wai.Test (defaultRequest)+import qualified Data.Vault.Lazy as Vault+import IHP.RequestVault (requestBodyVaultKey)+import Network.Wai (vault)+import qualified Data.List as List++-- Test types to simulate known types in error messages+data PageTitle = PageTitle deriving (Typeable)++tests = do+ let requestBody = FormBody [] [] ""+ let mockRequest = defaultRequest { vault = Vault.insert requestBodyVaultKey requestBody (vault defaultRequest) }+ let mockRespond :: Respond+ mockRespond = \_ -> pure ResponseReceived+ let ?request = mockRequest+ let ?respond = mockRespond+ describe "IHP.Controller.Context" do+ describe "putContext" do+ it "store a value" do+ context <- newControllerContext+ let ?context = context+ putContext ("hello" :: Text)++ it "fails if called on a frozen context" do+ context <- newControllerContext >>= freeze+ let ?context = context+ putContext ("hello" :: Text) `shouldThrow` anyException++ describe "fromContext" do+ it "return a stored value" do+ context <- newControllerContext+ let ?context = context++ putContext ("hello" :: Text)++ result <- fromContext @Text+ result `shouldBe` "hello"++ it "should fail if type not in container" do+ context <- newControllerContext+ let ?context = context++ (fromContext @Text) `shouldThrow` (\e -> case e of+ ErrorCall msg -> "Unable to find Text in controller context:" `List.isPrefixOf` msg+ _ -> False)++ it "return a stored value if frozen" do+ context <- newControllerContext+ let ?context = context++ putContext ("hello" :: Text)+ context <- freeze ?context+ let ?context = context++ result <- fromContext @Text+ result `shouldBe` "hello"++ describe "fromFrozenContext" do+ it "sohuld fail if not frozen" do+ context <- newControllerContext+ let ?context = context++ putContext ("hello" :: Text)++ let result = fromFrozenContext @Text+ (evaluate result) `shouldThrow` (errorCall "maybeFromFrozenContext called on a non frozen context while trying to access Text")++ it "return a stored value" do+ context <- newControllerContext+ let ?context = context++ putContext ("hello" :: Text)++ context <- freeze ?context++ let ?context = context++ (fromFrozenContext @Text) `shouldBe` "hello"++ it "should provide helpful error message for known types" do+ context <- newControllerContext+ let ?context = context++ -- Test that error message does not include a hint for unknown types+ (fromContext @Int) `shouldThrow` (\e -> case e of+ ErrorCall msg ->+ "Unable to find" `List.isPrefixOf` msg &&+ not ("Hint:" `List.isInfixOf` msg) -- Int is not a known type, so no hint+ _ -> False)++ it "should provide hint for PageTitle" do+ context <- newControllerContext+ let ?context = context++ -- Test that PageTitle gets the correct hint+ (fromContext @PageTitle) `shouldThrow` (\e -> case e of+ ErrorCall msg ->+ "Unable to find PageTitle in controller context:" `List.isPrefixOf` msg &&+ "Hint: Use 'setTitle' to set the page title (imported from IHP.PageHead.ControllerFunctions)" `List.isInfixOf` msg+ _ -> False)
@@ -0,0 +1,47 @@+{-|+Module: Test.Controller.CookieSpec+Copyright: (c) digitally induced GmbH, 2022+-}+module Test.Controller.CookieSpec where++import IHP.Prelude+import Test.Hspec++import Wai.Request.Params.Middleware (RequestBody (..), requestBodyVaultKey)+import qualified Data.Vault.Lazy as Vault+import IHP.Controller.Response (addResponseHeadersFromContext, responseHeadersVaultKey)+import IHP.Controller.Cookie+import IHP.Controller.Context+import qualified Network.Wai as Wai+import Web.Cookie+import Network.HTTP.Types.Status++tests = do+ describe "IHP.Controller.Cookie" do+ describe "setCookie" do+ it "set a 'Set-Cookie' header" do+ (context, request) <- createControllerContext+ let ?context = context+ let ?request = request++ setCookie defaultSetCookie+ { setCookieName = "exampleCookie"+ , setCookieValue = "exampleValue"+ }++ let response = Wai.responseLBS status200 [] "Hello World"+ responseWithHeaders <- addResponseHeadersFromContext response++ Wai.responseHeaders responseWithHeaders `shouldBe` [("Set-Cookie", "exampleCookie=exampleValue")]+++createControllerContext = do+ headersRef <- newIORef []+ let+ requestBody = FormBody { params = [], files = [], rawPayload = "" }+ request = Wai.defaultRequest { Wai.vault = Vault.insert requestBodyVaultKey requestBody+ . Vault.insert responseHeadersVaultKey headersRef+ $ Vault.empty }+ let ?request = request+ context <- newControllerContext+ pure (context, request)
@@ -0,0 +1,70 @@+{-|+Module: Test.Controller.NotFoundSpec+Tests for Not found functions.+-}+{-# LANGUAGE AllowAmbiguousTypes #-}++module Test.Controller.NotFoundSpec where+import ClassyPrelude+import Test.Hspec+import IHP.Test.Mocking+import IHP.Prelude+import IHP.Environment+import IHP.RouterSupport hiding (get)+import IHP.FrameworkConfig+import IHP.ViewPrelude+import IHP.ControllerPrelude hiding (get, request)+import Network.Wai.Test+import Network.HTTP.Types++data WebApplication = WebApplication deriving (Eq, Show, Data)++data TestController+ = TestActionNotFoundWhen+ | TestActionNotFoundUnless+ deriving (Eq, Show, Data)++instance Controller TestController where+ action TestActionNotFoundWhen = do+ notFoundWhen True+ renderPlain "Test"+ action TestActionNotFoundUnless = do+ notFoundUnless False+ renderPlain "Test"++instance AutoRoute TestController++instance FrontController WebApplication where+ controllers = [ parseRoute @TestController ]+++defaultLayout :: Html -> Html+defaultLayout inner = [hsx|{inner}|]++instance InitControllerContext WebApplication where+ initContext = do+ setLayout defaultLayout++instance FrontController RootApplication where+ controllers = [ mountFrontController WebApplication ]++testGet :: ByteString -> Session SResponse+testGet url = request $ setPath defaultRequest { requestMethod = methodGet } url+++config = do+ option Development+ option (AppPort 8000)++assertNotFound :: SResponse -> IO ()+assertNotFound response = do+ response.simpleStatus `shouldBe` status404+ response.simpleBody `shouldNotBe` "Test"++tests :: Spec+tests = aroundAll (withMockContextAndApp WebApplication config) do+ describe "Not found" $ do+ it "should return show 404 page when notFoundWhen is True" $ withContextAndApp \application -> do+ runSession (testGet "test/TestActionNotFoundWhen") application >>= assertNotFound+ it "should return show 404 page when notFoundUnless is False" $ withContextAndApp \application -> do+ runSession (testGet "test/TestActionNotFoundUnless") application >>= assertNotFound
@@ -0,0 +1,506 @@+{-|+Module: Test.Controller.ParamSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.Controller.ParamSpec where++import IHP.Prelude+import Test.Hspec+import IHP.Controller.Param+import IHP.Controller.Context+import Wai.Request.Params.Middleware (RequestBody (..), requestBodyVaultKey)+import qualified Data.Vault.Lazy as Vault+import IHP.ModelSupport+import Data.Bits ((.|.))+import qualified Data.Aeson as Aeson+import qualified Data.TMap as TypeMap+import qualified Network.Wai as Wai+import qualified GHC.IO as IO+import Data.Scientific (Scientific)+import Data.Either (isLeft)++tests = do+ describe "IHP.Controller.Param" do+ describe "param" do+ it "should parse valid input" do+ let ?context = createControllerContextWithParams [("page", "1")]+ let ?request = ?context.request+ (param @Int "page") `shouldBe` 1++ it "should fail on empty input" do+ let ?context = createControllerContextWithParams [("page", "")]+ let ?request = ?context.request+ (IO.evaluate (param @Int "page")) `shouldThrow` (== ParamCouldNotBeParsedException { name = "page", parserError = "has to be an integer" })++ it "should fail if param not provided" do+ let ?context = createControllerContextWithParams []+ let ?request = ?context.request+ (IO.evaluate (param @Int "page")) `shouldThrow` (== ParamNotFoundException { name = "page" })++ it "should fail with a parser error on invalid input" do+ let ?context = createControllerContextWithParams [("page", "NaN")]+ let ?request = ?context.request+ (IO.evaluate (param @Int "page")) `shouldThrow` (== ParamCouldNotBeParsedException { name = "page", parserError = "has to be an integer" })++ describe "paramOrNothing" do+ it "should parse valid input" do+ let ?context = createControllerContextWithParams [("referredBy", "776ab71d-327f-41b3-90a8-7b5a251c4b88")]+ let ?request = ?context.request+ (paramOrNothing @UUID "referredBy") `shouldBe` (Just "776ab71d-327f-41b3-90a8-7b5a251c4b88")++ it "should return Nothing on empty input" do+ let ?context = createControllerContextWithParams [("referredBy", "")]+ let ?request = ?context.request+ (paramOrNothing @UUID "referredBy") `shouldBe` Nothing++ it "should return Nothing if param not provided" do+ let ?context = createControllerContextWithParams []+ let ?request = ?context.request+ (paramOrNothing @UUID "referredBy") `shouldBe` Nothing++ it "should fail with a parser error on invalid input" do+ let ?context = createControllerContextWithParams [("referredBy", "not a uuid")]+ let ?request = ?context.request+ (IO.evaluate (paramOrNothing @UUID "referredBy")) `shouldThrow` (== ParamCouldNotBeParsedException { name = "referredBy", parserError = "has to be an UUID" })++ describe "paramOrDefault" do+ it "should parse valid input" do+ let ?context = createControllerContextWithParams [("page", "1")]+ let ?request = ?context.request+ (paramOrDefault @Int 0 "page") `shouldBe` 1++ it "should return default value on empty input" do+ let ?context = createControllerContextWithParams [("page", "")]+ let ?request = ?context.request+ (paramOrDefault @Int 10 "page") `shouldBe` 10++ it "should return default value if param not provided" do+ let ?context = createControllerContextWithParams []+ let ?request = ?context.request+ (paramOrDefault @Int 10 "page") `shouldBe` 10++ it "should fail with a parser error on invalid input" do+ let ?context = createControllerContextWithParams [("page", "NaN")]+ let ?request = ?context.request+ (IO.evaluate (paramOrDefault @Int 10 "page")) `shouldThrow` (== ParamCouldNotBeParsedException { name = "page", parserError = "has to be an integer" })+++ describe "paramList" do+ it "should parse valid input" do+ let ?context = createControllerContextWithParams [("ingredients", "milk"), ("ingredients", "egg")]+ let ?request = ?context.request+ (paramList @Text "ingredients") `shouldBe` ["milk", "egg"]++ it "should fail on invalid input" do+ let ?context = createControllerContextWithParams [("numbers", "1"), ("numbers", "NaN")]+ let ?request = ?context.request+ (IO.evaluate (paramList @Int "numbers")) `shouldThrow` (errorCall "param: Parameter 'numbers' is invalid")++ it "should deal with empty input" do+ let ?context = createControllerContextWithParams []+ let ?request = ?context.request+ (paramList @Int "numbers") `shouldBe` []++ describe "paramListOrNothing" do+ it "should parse valid input" do+ let ?context = createControllerContextWithParams [("ingredients", "milk"), ("ingredients", ""), ("ingredients", "egg")]+ let ?request = ?context.request+ (paramListOrNothing @Text "ingredients") `shouldBe` [Just "milk", Nothing, Just "egg"]++ it "should not fail on invalid input" do+ let ?context = createControllerContextWithParams [("numbers", "1"), ("numbers", "NaN")]+ let ?request = ?context.request+ (paramListOrNothing @Int "numbers") `shouldBe` [Just 1, Nothing]++ it "should deal with empty input" do+ let ?context = createControllerContextWithParams []+ let ?request = ?context.request+ (paramListOrNothing @Int "numbers") `shouldBe` []++ describe "hasParam" do+ it "returns True if param given" do+ let ?context = createControllerContextWithParams [("a", "test")]+ let ?request = ?context.request+ hasParam "a" `shouldBe` True++ it "returns True if param given but empty" do+ let ?context = createControllerContextWithParams [("a", "")]+ let ?request = ?context.request+ hasParam "a" `shouldBe` True++ it "returns False if param missing" do+ let ?context = createControllerContextWithParams []+ let ?request = ?context.request+ hasParam "a" `shouldBe` False++ describe "ParamReader" do+ describe "ByteString" do+ it "should handle text input" do+ (readParameter @ByteString "test") `shouldBe` (Right "test")+ it "should handle JSON strings" do+ (readParameterJSON @ByteString (json "\"test\"")) `shouldBe` (Right ("test" :: ByteString))++ it "should fail on other JSON input" do+ (readParameterJSON @ByteString (json "1")) `shouldBe` (Left ("Expected String" :: ByteString))++ describe "Int" do+ it "should accept numeric input" do+ (readParameter @Int "1337") `shouldBe` (Right 1337)++ it "should accept negative numbers" do+ (readParameter @Int "-1337") `shouldBe` (Right (-1337))++ it "should accept JSON numerics " do+ (readParameterJSON @Int (json "1337")) `shouldBe` (Right 1337)++ it "should fail on other JSON input " do+ (readParameterJSON @Int (json "true")) `shouldBe` (Left "Expected Int")++ describe "Integer" do+ it "should accept numeric input" do+ (readParameter @Integer "1337") `shouldBe` (Right 1337)++ it "should accept negative numbers" do+ (readParameter @Integer "-1337") `shouldBe` (Right (-1337))++ it "should accept JSON numerics " do+ (readParameterJSON @Integer (json "1337")) `shouldBe` (Right 1337)++ it "should fail on other JSON input " do+ (readParameterJSON @Integer (json "true")) `shouldBe` (Left "Expected Integer")+ (readParameterJSON @Integer (json "\"1\"")) `shouldBe` (Left "Expected Integer")+++ describe "Double" do+ it "should accept integer input" do+ (readParameter @Double "1337") `shouldBe` (Right 1337)++ it "should accept floating point input" do+ (readParameter @Double "1.2") `shouldBe` (Right 1.2)+ (readParameter @Double "1.2345679") `shouldBe` (Right 1.2345679)++ it "should accept JSON integer input" do+ (readParameterJSON @Double (json "1337")) `shouldBe` (Right 1337)++ it "should accept JSON floating point input" do+ (readParameterJSON @Double (json "1.2")) `shouldBe` (Right 1.2)++ it "should fail on other JSON input " do+ (readParameterJSON @Double (json "true")) `shouldBe` (Left "Expected Double")+ (readParameterJSON @Double (json "\"1\"")) `shouldBe` (Left "Expected Double")++ describe "Scientific" do+ it "should accept integer input" do+ (readParameter @Scientific "1337") `shouldBe` (Right 1337)++ it "should accept floating point input" do+ (readParameter @Scientific "1.2") `shouldBe` (Right 1.2)+ (readParameter @Scientific "1.2345679") `shouldBe` (Right 1.2345679)+ let x = "1e-1024" -- -1024 is smaller than minimal Double exponent of -1021+ y = "1.0e-1024"+ (show <$> readParameter @Scientific x) `shouldBe` (Right y)++ it "should accept JSON integer input" do+ (readParameterJSON @Scientific (json "1337")) `shouldBe` (Right 1337)++ it "should accept JSON floating point input" do+ (readParameterJSON @Scientific (json "1.2")) `shouldBe` (Right 1.2)++ it "should fail on other JSON input " do+ (readParameterJSON @Scientific (json "true")) `shouldBe` (Left "Expected Scientific")+ (readParameterJSON @Scientific (json "\"1\"")) `shouldBe` (Left "Expected Scientific")++ describe "Float" do+ it "should accept integer input" do+ (readParameter @Float "1337") `shouldBe` (Right 1337)++ it "should accept floating point input" do+ (readParameter @Float "1.2") `shouldBe` (Right 1.2)+ (readParameter @Float "1.2345679") `shouldBe` (Right 1.2345679)++ it "should accept JSON integer input" do+ (readParameterJSON @Float (json "1337")) `shouldBe` (Right 1337)++ it "should accept JSON floating point input" do+ (readParameterJSON @Float (json "1.2")) `shouldBe` (Right 1.2)++ it "should fail on other JSON input " do+ (readParameterJSON @Float (json "true")) `shouldBe` (Left "Expected Float")+ (readParameterJSON @Float (json "\"1\"")) `shouldBe` (Left "Expected Float")++ describe "Point" do+ it "should accept integer input" do+ (readParameter @Point "1337,1338") `shouldBe` (Right (fromCoordinates 1337 1338))++ it "should accept floating point input" do+ (readParameter @Point "1.2,1.3") `shouldBe` (Right (fromCoordinates 1.2 1.3))++ it "should accept JSON integer input" do+ (readParameterJSON @Point (json "\"1337,1338\"")) `shouldBe` (Right (fromCoordinates 1337 1338))++ it "should accept JSON floating point input" do+ (readParameterJSON @Point (json "\"1.2,1.3\"")) `shouldBe` (Right (fromCoordinates 1.2 1.3))++ it "should fail on other JSON input " do+ (readParameterJSON @Point (json "true")) `shouldBe` (Left "Expected Point")+ (readParameterJSON @Point (json "\"1\"")) `shouldBe` (Left "has to be two numbers with a comma, e.g. '1,2'")+ (readParameterJSON @Point (json "\"1.2\"")) `shouldBe` (Left "has to be two numbers with a comma, e.g. '1,2'")++ describe "Polygon" do+ let mkPolygon pts = case refineFromPointList pts of+ Just p -> p+ Nothing -> error "test: invalid polygon"++ it "should accept integer input" do+ (readParameter @Polygon "(100,200),(300,400),(500,600)") `shouldBe`+ (Right (mkPolygon [(100, 200), (300, 400), (500, 600)]))++ it "should accept floating-point input" do+ (readParameter @Polygon "(100.1,200.2),(300.3,400.4),(500.5,600.6)") `shouldBe`+ (Right (mkPolygon [(100.1, 200.2), (300.3, 400.4), (500.5, 600.6)]))++ it "should accept JSON integer input" do+ (readParameterJSON @Polygon (json "\"(100,200),(300,400),(500,600)\"")) `shouldBe`+ (Right (mkPolygon [(100, 200), (300, 400), (500, 600)]))++ it "should accept JSON floating-point input" do+ (readParameterJSON @Polygon (json "\"(100.1,200.2),(300.3,400.4),(500.5,600.6)\"")) `shouldBe`+ (Right (mkPolygon [(100.1, 200.2), (300.3, 400.4), (500.5, 600.6)]))++ it "should reject polygon with fewer than 3 points" do+ (readParameter @Polygon "(100,200),(300,400)") `shouldSatisfy` isLeft++ describe "Text" do+ it "should handle text input" do+ (readParameter @Text "test") `shouldBe` (Right "test")++ it "should handle JSON strings" do+ (readParameterJSON @Text (json "\"test\"")) `shouldBe` (Right ("test"))++ it "should fail on other JSON input" do+ (readParameterJSON @Text (json "1")) `shouldBe` (Left ("Expected String"))++ describe "CSV" do+ it "should handle empty input" do+ (readParameter @[Int] "") `shouldBe` (Right [])++ it "should handle a single value" do+ (readParameter @[Int] "1") `shouldBe` (Right [1])++ it "should handle comma separated values" do+ (readParameter @[Int] "1,2,3") `shouldBe` (Right [1,2,3])++ it "should fail if a single value is invalid" do+ (readParameter @[Int] "1,a,3") `shouldBe` (Left "has to be an integer")++ it "should handle JSON arrays" do+ (readParameterJSON @[Int] (json "[1,2,3]")) `shouldBe` (Right [1,2,3])++ it "should fail on JSON input that is not an array" do+ (readParameterJSON @[Int] (json "true")) `shouldBe` (Left "Expected Array")++ describe "Bool" do+ it "should accept 'on' as True" do+ (readParameter @Bool "on") `shouldBe` (Right True)++ it "should accept 'true' as True" do+ (readParameter @Bool "true") `shouldBe` (Right True)+ (readParameter @Bool "TruE") `shouldBe` (Right True)++ it "should accept everything else as false input" do+ (readParameter @Bool "off") `shouldBe` (Right False)+ (readParameter @Bool "false") `shouldBe` (Right False)+ (readParameter @Bool "invalid") `shouldBe` (Right False)++ describe "UUID" do+ it "should accept UUID values" do+ (readParameter @UUID "6188329c-6bad-47f6-800c-2fd19ce0b2df") `shouldBe` (Right "6188329c-6bad-47f6-800c-2fd19ce0b2df")+ (readParameter @UUID "a020ba17-a94e-453f-9414-c54aa30caa54") `shouldBe` (Right "a020ba17-a94e-453f-9414-c54aa30caa54")++ it "should fail on invalid values" do+ (readParameter @UUID "not a uuid") `shouldBe` (Left "has to be an UUID")++ it "should accept JSON UUIDs" do+ (readParameterJSON @UUID (json "\"6188329c-6bad-47f6-800c-2fd19ce0b2df\"")) `shouldBe` (Right "6188329c-6bad-47f6-800c-2fd19ce0b2df")++ it "should fail on invalid JSON input" do+ (readParameterJSON @UUID (json "\"not a uuid\"")) `shouldBe` (Left "Invalid UUID")+ (readParameterJSON @UUID (json "false")) `shouldBe` (Left "Expected String with an UUID")++ describe "UTCTime" do+ it "should accept timestamps" do+ (tshow (readParameter @UTCTime "2020-11-08T12:03:35Z")) `shouldBe` ("Right 2020-11-08 12:03:35 UTC")++ it "should accept dates" do+ (tshow (readParameter @UTCTime "2020-11-08")) `shouldBe` ("Right 2020-11-08 00:00:00 UTC")++ it "should fail on invalid inputs" do+ (readParameter @UTCTime "not a timestamp") `shouldBe` (Left "has to be a valid date and time, e.g. 2020-11-08T12:03:35Z or 2020-11-08T12:03")++ it "should accept JSON strings" do+ (tshow (readParameterJSON @UTCTime (json "\"2020-11-08T12:03:35Z\""))) `shouldBe` ("Right 2020-11-08 12:03:35 UTC")++ describe "LocalTime" do+ it "should accept timestamps" do+ (tshow (readParameter @LocalTime "2020-11-08T12:03:35Z")) `shouldBe` ("Right 2020-11-08 12:03:35")++ it "should accept dates" do+ (tshow (readParameter @LocalTime "2020-11-08")) `shouldBe` ("Right 2020-11-08 00:00:00")++ it "should fail on invalid inputs" do+ (readParameter @LocalTime "not a timestamp") `shouldBe` (Left "has to be a valid date and time, e.g. 2020-11-08T12:03:35Z or 2020-11-08T12:03")++ it "should accept JSON strings" do+ (tshow (readParameterJSON @LocalTime (json "\"2020-11-08T12:03:35Z\""))) `shouldBe` ("Right 2020-11-08 12:03:35")++ describe "Day" do+ it "should accept dates" do+ (tshow (readParameter @Day "2020-11-08")) `shouldBe` ("Right 2020-11-08")++ it "should fail on invalid inputs" do+ (readParameter @Day "not a timestamp") `shouldBe` (Left "has to be a date, e.g. 2020-11-08")++ it "should accept JSON strings" do+ (tshow (readParameterJSON @Day (json "\"2020-11-08\""))) `shouldBe` ("Right 2020-11-08")++ describe "TimeOfDay" do+ it "should accept time values" do+ (tshow (readParameter @TimeOfDay "12:00:00")) `shouldBe` ("Right 12:00:00")++ it "should fail on invalid inputs" do+ (readParameter @TimeOfDay "not a time") `shouldBe` (Left "has to be time in the format hh:mm:ss")+ (readParameter @TimeOfDay "25:00:00") `shouldBe` (Left "has to be time in the format hh:mm:ss")++ it "should accept JSON strings" do+ (tshow (readParameterJSON @TimeOfDay (json "\"13:37:00\""))) `shouldBe` ("Right 13:37:00")++ describe "Maybe" do+ it "should accept values" do+ (readParameter @(Maybe Int) "1") `shouldBe` (Right (Just 1))+ (readParameter @(Maybe Text) "hello") `shouldBe` (Right (Just "hello"))++ it "should handle empty input as Nothing" do+ (readParameter @(Maybe Int) "") `shouldBe` (Right Nothing)+ (readParameter @(Maybe UUID) "") `shouldBe` (Right Nothing)+ (readParameterJSON @(Maybe Bool) "") `shouldBe` (Right Nothing)++ it "should handle empty Text as Just" do+ (readParameter @(Maybe Text) "") `shouldBe` (Right (Just ""))+ (readParameter @(Maybe ByteString) "") `shouldBe` (Right (Just ""))++ it "should handle empty Bool as False" do+ (readParameter @(Maybe Bool) "") `shouldBe` (Right (Just False))++ it "should deal with parser errors" do+ (readParameter @(Maybe Int) "not a number") `shouldBe` (Left "has to be an integer")++ describe "Enum" do+ it "should accept values" do+ (readParameter "Yellow") `shouldBe` (Right Yellow)+ (readParameter "Red") `shouldBe` (Right Red)+ (readParameter "Blue") `shouldBe` (Right Blue)++ it "should fail on invalid values" do+ (readParameter @Color "black") `shouldBe` (Left "Invalid value")+ (readParameter @Color "") `shouldBe` (Left "Invalid value")++ it "should deal with JSON" do+ (readParameterJSON (json "\"Yellow\"")) `shouldBe` (Right Yellow)+ (readParameterJSON (json "\"Red\"")) `shouldBe` (Right Red)+ (readParameterJSON (json "\"Blue\"")) `shouldBe` (Right Blue)++ it "should fail on invalid JSON" do+ (readParameterJSON @Color (json "\"\"")) `shouldBe` (Left "Invalid value")+ (readParameterJSON @Color (json "1337")) `shouldBe` (Left "enumParamReaderJSON: Invalid value, expected a string but got something else")++ describe "fill" do+ it "should fill provided values if valid" do+ let ?context = createControllerContextWithParams [("boolField", "on"), ("colorField", "Red")]+ let ?request = ?context.request++ let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }+ let expectedRecord = FillRecord { boolField = True, colorField = Red, meta = def { touchedFields = 3 } }++ let filledRecord = emptyRecord |> fill @["boolField", "colorField"]+ filledRecord `shouldBe` expectedRecord++ it "should not touch fields if a field is missing" do+ let ?context = createControllerContextWithParams [("colorField", "Red")]+ let ?request = ?context.request++ let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }+ let expectedRecord = FillRecord { boolField = False, colorField = Red, meta = def { touchedFields = 2 } }++ let filledRecord = emptyRecord |> fill @["boolField", "colorField"]+ filledRecord `shouldBe` expectedRecord++ it "should add validation errors if the parsing fails" do+ let ?context = createControllerContextWithParams [("colorField", "invalid color")]+ let ?request = ?context.request++ let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }+ let expectedRecord = FillRecord { boolField = False, colorField = Yellow, meta = def { annotations = [("colorField", TextViolation "Invalid value")] } }++ let filledRecord = emptyRecord |> fill @["boolField", "colorField"]+ filledRecord `shouldBe` expectedRecord++ it "should deal with json values" do+ let ?context = createControllerContextWithJson "{\"colorField\":\"Red\",\"boolField\":true}"+ let ?request = ?context.request++ let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }+ let expectedRecord = FillRecord { boolField = True, colorField = Red, meta = def { touchedFields = 3 } }++ let filledRecord = emptyRecord |> fill @["boolField", "colorField"]+ filledRecord `shouldBe` expectedRecord++ it "should deal with empty json values" do+ let ?context = createControllerContextWithJson "{}"+ let ?request = ?context.request++ let emptyRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }+ let expectedRecord = FillRecord { boolField = False, colorField = Yellow, meta = def }++ let filledRecord = emptyRecord |> fill @["boolField", "colorField"]+ filledRecord `shouldBe` expectedRecord++createControllerContextWithParams params =+ let+ requestBody = FormBody { params, files = [], rawPayload = "" }+ request = Wai.defaultRequest { Wai.vault = Vault.insert requestBodyVaultKey requestBody Vault.empty }+ customFields = TypeMap.insert request TypeMap.empty+ in FrozenControllerContext { customFields }++createControllerContextWithJson params =+ let+ requestBody = JSONBody { jsonPayload = Just (json params), rawPayload = cs params }+ request = Wai.defaultRequest { Wai.vault = Vault.insert requestBodyVaultKey requestBody Vault.empty }+ customFields = TypeMap.insert request TypeMap.empty+ in FrozenControllerContext { customFields }++json :: Text -> Aeson.Value+json string =+ let (Just value) :: Maybe Aeson.Value = Aeson.decode (cs string)+ in value++data Color = Yellow | Red | Blue deriving (Enum, Show, Eq)+instance ParamReader Color where+ readParameter = enumParamReader+ readParameterJSON = enumParamReaderJSON+instance InputValue Color where inputValue = tshow+++data FillRecord = FillRecord { boolField :: Bool, colorField :: Color, meta :: MetaBag }+ deriving (Show, Eq)++instance SetField "boolField" FillRecord Bool where+ setField value record = record { boolField = value, meta = (record.meta) { touchedFields = touchedFields record.meta .|. 1 } }++instance SetField "colorField" FillRecord Color where+ setField value record = record { colorField = value, meta = (record.meta) { touchedFields = touchedFields record.meta .|. 2 } }++instance SetField "meta" FillRecord MetaBag where+ setField value record = record { meta = value }++instance FieldBit "boolField" FillRecord where fieldBit = 1+instance FieldBit "colorField" FillRecord where fieldBit = 2
@@ -0,0 +1,118 @@+{-|+Module: Test.ControllerSupportSpec+-}+module Test.ControllerSupportSpec where++import IHP.Prelude+import Test.Hspec+import IHP.ControllerSupport (requestBodyJSON)+import IHP.Controller.Response (ResponseException(..))+import IHP.Environment (Environment (..))+import qualified IHP.FrameworkConfig as FrameworkConfig+import qualified IHP.RequestVault as RequestVault+import Wai.Request.Params.Middleware (RequestBody (..), requestBodyVaultKey)+import qualified Data.Vault.Lazy as Vault+import qualified Data.Aeson as Aeson+import qualified Network.Wai as Wai+import qualified Control.Exception as Exception+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString as BS+import Network.HTTP.Types (status400)+import System.IO.Unsafe (unsafePerformIO)+import Data.ByteString.Builder (toLazyByteString)++tests = do+ describe "IHP.ControllerSupport" do+ describe "requestBodyJSON" do+ it "should return parsed JSON value for valid JSONBody" do+ let jsonValue = Aeson.object [("name", Aeson.String "test")]+ let requestBody = JSONBody { jsonPayload = Just jsonValue, rawPayload = "{\"name\":\"test\"}" }+ request <- buildRequest requestBody Development+ let ?request = request+ result <- requestBodyJSON+ result `shouldBe` jsonValue++ it "should return 400 for FormBody" do+ let requestBody = FormBody { params = [], files = [], rawPayload = "" }+ request <- buildRequest requestBody Development+ let ?request = request+ result <- Exception.try requestBodyJSON+ case result of+ Left (ResponseException response) -> do+ Wai.responseStatus response `shouldBe` status400+ let body = responseBody response+ body `shouldSatisfy` bodyContains "form content type"+ Right _ -> expectationFailure "Expected ResponseException"++ it "should return 400 for JSONBody with empty body" do+ let requestBody = JSONBody { jsonPayload = Nothing, rawPayload = "" }+ request <- buildRequest requestBody Development+ let ?request = request+ result <- Exception.try requestBodyJSON+ case result of+ Left (ResponseException response) -> do+ Wai.responseStatus response `shouldBe` status400+ let body = responseBody response+ body `shouldSatisfy` bodyContains "request body is empty"+ Right _ -> expectationFailure "Expected ResponseException"++ it "should return 400 for JSONBody with invalid JSON" do+ let requestBody = JSONBody { jsonPayload = Nothing, rawPayload = "not valid json" }+ request <- buildRequest requestBody Development+ let ?request = request+ result <- Exception.try requestBodyJSON+ case result of+ Left (ResponseException response) -> do+ Wai.responseStatus response `shouldBe` status400+ let body = responseBody response+ body `shouldSatisfy` bodyContains "not valid json"+ Right _ -> expectationFailure "Expected ResponseException"++ it "should truncate long payloads in dev mode" do+ let longPayload = LBS.pack (replicate 500 65) -- 500 bytes of 'A'+ let requestBody = JSONBody { jsonPayload = Nothing, rawPayload = longPayload }+ request <- buildRequest requestBody Development+ let ?request = request+ result <- Exception.try requestBodyJSON+ case result of+ Left (ResponseException response) -> do+ let body = responseBody response+ body `shouldSatisfy` bodyContains "truncated"+ body `shouldSatisfy` bodyContains "raw request body was"+ Right _ -> expectationFailure "Expected ResponseException"++ it "should omit raw payload in production mode" do+ let requestBody = JSONBody { jsonPayload = Nothing, rawPayload = "not valid json" }+ request <- buildRequest requestBody Production+ let ?request = request+ result <- Exception.try requestBodyJSON+ case result of+ Left (ResponseException response) -> do+ Wai.responseStatus response `shouldBe` status400+ let body = responseBody response+ body `shouldSatisfy` (not . bodyContains "not valid json")+ body `shouldSatisfy` (not . bodyContains "raw request body was")+ Right _ -> expectationFailure "Expected ResponseException"++bodyContains :: BS.ByteString -> LBS.ByteString -> Bool+bodyContains needle haystack = BS.isInfixOf needle (LBS.toStrict haystack)++buildRequest :: RequestBody -> Environment -> IO Wai.Request+buildRequest requestBody environment = do+ frameworkConfig <- FrameworkConfig.buildFrameworkConfig (FrameworkConfig.option environment)+ pure Wai.defaultRequest+ { Wai.vault = Vault.insert RequestVault.frameworkConfigVaultKey frameworkConfig+ $ Vault.insert requestBodyVaultKey requestBody Vault.empty+ }++-- | Extract the body from a WAI Response (works for responseLBS responses)+responseBody :: Wai.Response -> LBS.ByteString+responseBody response =+ let (_, _, withBody) = Wai.responseToStream response+ in unsafePerformIO $ withBody $ \streamingBody -> do+ ref <- newIORef mempty+ streamingBody+ (\chunk -> modifyIORef ref (<> chunk))+ (pure ())+ builder <- readIORef ref+ pure (toLazyByteString builder)
@@ -0,0 +1,147 @@+{-# LANGUAGE ApplicativeDo #-}+{-|+Module: Test.FetchPipelinedSpec+Description: Integration tests for IHP.FetchPipelined+Copyright: (c) digitally induced GmbH, 2026+-}+module Test.FetchPipelinedSpec where++import Test.Hspec+import IHP.Prelude+import IHP.QueryBuilder+import IHP.ModelSupport+import IHP.Hasql.FromRow (FromRowHasql(..), HasqlDecodeColumn(..))+import IHP.FetchPipelined+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Session as Session+import qualified IHP.Log as Log+import IHP.Log.Types (LogLevel(..), LoggerSettings(..))+import System.Environment (lookupEnv)+import qualified Control.Exception as Exception++-- Model mapping to a temp table created during tests++data FpItem = FpItem+ { id :: UUID+ , name :: Text+ , active :: Bool+ }+ deriving (Show, Eq)++type instance GetTableName FpItem = "fp_items"+type instance GetModelByTableName "fp_items" = FpItem+type instance PrimaryKey "fp_items" = UUID++instance Table FpItem where+ columnNames = ["id", "name", "active"]+ primaryKeyColumnNames = ["id"]++instance FromRowHasql FpItem where+ hasqlRowDecoder = FpItem+ <$> hasqlColumnDecoder+ <*> hasqlColumnDecoder+ <*> hasqlColumnDecoder++-- | Run a test with a database connection, creating/dropping a temp table.+-- Skips if PostgreSQL is not available.+withDB :: (ModelContext -> IO ()) -> IO ()+withDB action = do+ envUrl <- lookupEnv "DATABASE_URL"+ let databaseUrl = maybe "postgresql:///postgres" cs envUrl+ logger <- Log.newLogger def { level = Warn }+ modelContext <- createModelContext databaseUrl logger+ let pool = modelContext.hasqlPool+ let setup = do+ result <- HasqlPool.use pool $ Session.script+ "CREATE TABLE IF NOT EXISTS fp_items (id UUID PRIMARY KEY, name TEXT NOT NULL, active BOOL NOT NULL);\+ \INSERT INTO fp_items (id, name, active) VALUES \+ \('a0000000-0000-0000-0000-000000000001', 'alpha', true),\+ \('a0000000-0000-0000-0000-000000000002', 'beta', true),\+ \('a0000000-0000-0000-0000-000000000003', 'gamma', false);"+ case result of+ Left err -> Exception.throwIO (HasqlError err)+ Right () -> pure ()+ let teardown = do+ _ <- HasqlPool.use pool $ Session.script "DROP TABLE IF EXISTS fp_items;"+ releaseModelContext modelContext+ result <- Exception.try (setup >> action modelContext `Exception.finally` teardown)+ case result of+ Right () -> pure ()+ Left (HasqlError (HasqlPool.ConnectionUsageError _)) ->+ pendingWith "PostgreSQL not available (set DATABASE_URL or start a local Postgres)"+ Left e -> Exception.throwIO e++tests :: Spec+tests = do+ describe "IHP.FetchPipelined" do+ describe "pipeline" do+ it "should fetch all rows with fetchPipelined" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ items <- pipeline $+ query @FpItem |> fetchPipelined+ length items `shouldBe` 3++ it "should apply filterWhere" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ items <- pipeline $+ query @FpItem |> filterWhere (#active, True) |> fetchPipelined+ length items `shouldBe` 2++ it "should fetch multiple queries in one pipeline" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ (allItems, activeItems) <- pipeline do+ allItems <- query @FpItem |> fetchPipelined+ activeItems <- query @FpItem |> filterWhere (#active, True) |> fetchPipelined+ pure (allItems, activeItems)+ length allItems `shouldBe` 3+ length activeItems `shouldBe` 2++ it "should pipeline three queries" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ (items, count, exists) <- pipeline do+ items <- query @FpItem |> filterWhere (#active, True) |> fetchPipelined+ count <- query @FpItem |> fetchCountPipelined+ exists <- query @FpItem |> filterWhere (#active, False) |> fetchExistsPipelined+ pure (items, count, exists)+ length items `shouldBe` 2+ count `shouldBe` 3+ exists `shouldBe` True++ describe "fetchOneOrNothingPipelined" do+ it "should return Just for matching row" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ result <- pipeline $+ query @FpItem |> filterWhere (#name, "alpha" :: Text) |> fetchOneOrNothingPipelined+ ((.name) <$> result) `shouldBe` Just "alpha"++ it "should return Nothing for no match" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ result <- pipeline $+ query @FpItem |> filterWhere (#name, "nonexistent" :: Text) |> fetchOneOrNothingPipelined+ result `shouldBe` Nothing++ describe "fetchCountPipelined" do+ it "should count all rows" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ count <- pipeline $+ query @FpItem |> fetchCountPipelined+ count `shouldBe` 3++ it "should count filtered rows" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ count <- pipeline $+ query @FpItem |> filterWhere (#active, False) |> fetchCountPipelined+ count `shouldBe` 1++ describe "fetchExistsPipelined" do+ it "should return True when rows exist" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ exists <- pipeline $+ query @FpItem |> fetchExistsPipelined+ exists `shouldBe` True++ it "should return False when no rows match" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ exists <- pipeline $+ query @FpItem |> filterWhere (#name, "nonexistent" :: Text) |> fetchExistsPipelined+ exists `shouldBe` False
@@ -0,0 +1,101 @@+module Test.FileStorage.ControllerFunctionsSpec where++import Test.Hspec+import IHP.Prelude+import IHP.FileStorage.ControllerFunctions+import IHP.Controller.Context+import IHP.FrameworkConfig+import Network.Wai as Wai (defaultRequest)+import qualified Network.Wai as Wai+import Network.Wai.Parse (FileInfo(..))+import Wai.Request.Params.Middleware (RequestBody (..))+import IHP.FileStorage.Types+import IHP.FileStorage.Config+import qualified Data.Vault.Lazy as Vault+import qualified IHP.RequestVault++tests :: Spec+tests = describe "IHP.FileStorage.ControllerFunctions" $ do++ let config :: ConfigBuilder+ config = do+ initStaticDirStorage++ let withFrameworkConfig = IHP.FrameworkConfig.withFrameworkConfig config++ describe "storeFileWithOptions" $ do+ it "returns the objectPath without the baseUrl" $ do+ withFrameworkConfig \frameworkConfig -> do+ context <- createControllerContext frameworkConfig+ let ?context = context++ let fileInfo = FileInfo+ { fileName = "test.txt"+ , fileContentType = "text/plain"+ , fileContent = "Hello, world!"+ }++ -- We pass the UUID that will be used as the filename, so we can easily assert the objectPath.+ let options :: StoreFileOptions = def+ { fileName = Just "4c55dac2-e411-45ac-aa10-b957b01221df"+ , directory = "Test.FileStorage.ControllerFunctionsSpec"+ }++ result <- storeFileWithOptions fileInfo options++ result.url `shouldBe` ("/Test.FileStorage.ControllerFunctionsSpec/4c55dac2-e411-45ac-aa10-b957b01221df")++ describe "contentDispositionAttachmentAndFileName" $ do+ let fileInfoWith name = FileInfo { fileName = name, fileContentType = "application/octet-stream", fileContent = "" }++ it "produces RFC 5987 encoded header for a plain ASCII filename" $ do+ result <- contentDispositionAttachmentAndFileName (fileInfoWith "report.pdf")+ result `shouldBe` Just "attachment; filename*=UTF-8''report.pdf"++ it "percent-encodes spaces in the filename" $ do+ result <- contentDispositionAttachmentAndFileName (fileInfoWith "my report.pdf")+ result `shouldBe` Just "attachment; filename*=UTF-8''my%20report.pdf"++ it "percent-encodes special characters in the filename" $ do+ result <- contentDispositionAttachmentAndFileName (fileInfoWith "file (final).pdf")+ result `shouldBe` Just "attachment; filename*=UTF-8''file%20%28final%29.pdf"++ it "percent-encodes non-ASCII characters in the filename" $ do+ result <- contentDispositionAttachmentAndFileName (fileInfoWith (cs ("ファイル.pdf" :: Text)))+ result `shouldBe` Just "attachment; filename*=UTF-8''%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.pdf"++ describe "createTemporaryDownloadUrlFromPath" $ do+ it "returns baseUrl concatenated with objectPath when objectPath does not start with http:// or https://" $ do+ withFrameworkConfig \frameworkConfig -> do+ context <- createControllerContext frameworkConfig+ let ?context = context+ let objectPath = "static/test.txt"+ temporaryDownloadUrl <- createTemporaryDownloadUrlFromPath objectPath++ temporaryDownloadUrl.url `shouldBe` "http://localhost:8000/static/test.txt"++ it "returns baseUrl concatenated with objectPath without double slash when objectPath starts with '/'" $ do+ withFrameworkConfig \frameworkConfig -> do+ context <- createControllerContext frameworkConfig+ let ?context = context+ let objectPath = "/static/test.txt"+ temporaryDownloadUrl <- createTemporaryDownloadUrlFromPath objectPath++ temporaryDownloadUrl.url `shouldBe` "http://localhost:8000/static/test.txt"++ it "returns the objectPath when objectPath starts with 'http://' or 'https://'" $ do+ withFrameworkConfig \frameworkConfig -> do+ context <- createControllerContext frameworkConfig+ let ?context = context+ let objectPath = "https://example.com/static/test.txt"+ temporaryDownloadUrl <- createTemporaryDownloadUrlFromPath objectPath++ temporaryDownloadUrl.url `shouldBe` "https://example.com/static/test.txt"++createControllerContext frameworkConfig = do+ let+ requestBody = FormBody { params = [], files = [], rawPayload = "" }+ request = Wai.defaultRequest { Wai.vault = Vault.insert IHP.RequestVault.frameworkConfigVaultKey frameworkConfig+ $ Vault.insert IHP.RequestVault.requestBodyVaultKey requestBody Vault.empty }+ let ?request = request+ newControllerContext
@@ -0,0 +1,59 @@+{-|+Module: Test.HaskellSupportSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.HaskellSupportSpec where++import Test.Hspec+import IHP.Prelude++tests = do+ describe "HaskellSupport" do+ describe "stripTags" do+ it "should deal with empty input" do+ stripTags "" `shouldBe` ""++ it "should strip html tags and return the plain text" do+ stripTags "This is <b>Bold</b>" `shouldBe` "This is Bold"++ describe "copyFields" do+ it "should copy fields" do+ let a = RecordA { fieldA = 1, fieldB = "test" }+ let b = RecordB { fieldA = 0, fieldB = "" }++ let b' = b |> copyFields @["fieldA", "fieldB"] a++ b' `shouldBe` RecordB { fieldA = 1, fieldB = "test" }++ describe "allEnumValues" do+ it "should return all enum values" do+ (allEnumValues @Color) `shouldBe` [Yellow, Red, Blue]++ describe "setMaybe" do+ it "should set a Just value" do+ let c = RecordC { field = Nothing }+ let c' = c |> setMaybe #field (Just 1)++ c' `shouldBe` RecordC { field = Just 1 }++ it "should not set a Nothing value" do+ let c = RecordC { field = Just 1 }+ let c' = c |> setMaybe #field Nothing++ c' `shouldBe` RecordC { field = Just 1 }+++data RecordA = RecordA { fieldA :: Int, fieldB :: Text } deriving (Eq, Show)+data RecordB = RecordB { fieldA :: Int, fieldB :: Text } deriving (Eq, Show)+data RecordC = RecordC { field :: Maybe Int } deriving (Eq, Show)++instance SetField "fieldA" RecordB Int where+ setField value record = record { fieldA = value }++instance SetField "fieldB" RecordB Text where+ setField value record = record { fieldB = value }++instance SetField "field" RecordC (Maybe Int) where+ setField value record = record { field = value }++data Color = Yellow | Red | Blue deriving (Enum, Show, Eq)
@@ -0,0 +1,112 @@+module Test.HasqlEncoderSpec where++import Test.Hspec+import IHP.Prelude+import IHP.Hasql.Encoders (ToSnippetParams(..), sqlToSnippet)+import IHP.ModelSupport.Types (Id'(..), PrimaryKey)+import qualified Hasql.DynamicStatements.Snippet as Snippet+import Hasql.DynamicStatements.Snippet (Snippet)+import qualified Hasql.Statement as Statement+import qualified Hasql.Decoders as Decoders+import Hasql.Implicits.Encoders ()+import Database.PostgreSQL.Simple (Only(..), (:.)(..))++-- Test type family instances for non-UUID primary keys+type instance PrimaryKey "countries" = Text+type instance PrimaryKey "serial_table" = Int+type instance PrimaryKey "users" = UUID++-- | Convert a Snippet to its SQL text representation for testing.+-- Parameters become $1, $2, etc.+snippetToSql :: Snippet -> Text+snippetToSql s = Statement.toSql (Snippet.toStatement s Decoders.noResult)++tests :: Spec+tests = do+ describe "Hasql Encoders" do+ describe "sqlToSnippet" do+ it "should handle SQL with no placeholders" do+ snippetToSql (sqlToSnippet "SELECT 1" [])+ `shouldBe` "SELECT 1"++ it "should convert a single ? placeholder to $1" do+ snippetToSql (sqlToSnippet "SELECT * FROM users WHERE id = ?" [Snippet.param (42 :: Int64)])+ `shouldBe` "SELECT * FROM users WHERE id = $1"++ it "should convert multiple ? placeholders to $1, $2" do+ snippetToSql (sqlToSnippet "WHERE id = ? AND name = ?" [Snippet.param (1 :: Int64), Snippet.param ("test" :: Text)])+ `shouldBe` "WHERE id = $1 AND name = $2"++ it "should handle placeholders inside parentheses" do+ snippetToSql (sqlToSnippet "INSERT INTO t VALUES (?)" [Snippet.param (1 :: Int64)])+ `shouldBe` "INSERT INTO t VALUES ($1)"++ it "should handle adjacent placeholders" do+ snippetToSql (sqlToSnippet "VALUES (?, ?)" [Snippet.param (1 :: Int64), Snippet.param (2 :: Int64)])+ `shouldBe` "VALUES ($1, $2)"++ describe "ToSnippetParams" do+ it "should produce 0 params for ()" do+ length (toSnippetParams ()) `shouldBe` 0++ it "should produce 1 param for Only" do+ length (toSnippetParams (Only (42 :: Int64))) `shouldBe` 1++ it "should produce 2 params for a 2-tuple" do+ length (toSnippetParams ("a" :: Text, "b" :: Text)) `shouldBe` 2++ it "should produce 3 params for a 3-tuple" do+ length (toSnippetParams ("a" :: Text, "b" :: Text, "c" :: Text)) `shouldBe` 3++ it "should concatenate params with :." do+ length (toSnippetParams (Only (1 :: Int64) :. Only (2 :: Int64))) `shouldBe` 2++ it "should concatenate tuple + Only with :." do+ length (toSnippetParams ((1 :: Int64, 2 :: Int64) :. Only (3 :: Int64))) `shouldBe` 3++ describe "sqlToSnippet + toSnippetParams end-to-end" do+ it "should parameterize a single-param query" do+ snippetToSql (sqlToSnippet "SELECT ?" (toSnippetParams (Only (42 :: Int64))))+ `shouldBe` "SELECT $1"++ it "should parameterize a two-param query" do+ snippetToSql (sqlToSnippet "SELECT ?, ?" (toSnippetParams ("a" :: Text, "b" :: Text)))+ `shouldBe` "SELECT $1, $2"++ it "should handle no-param query" do+ snippetToSql (sqlToSnippet "SELECT 1" (toSnippetParams ()))+ `shouldBe` "SELECT 1"++ it "should parameterize a three-param query" do+ snippetToSql (sqlToSnippet "WHERE a = ? AND b = ? AND c = ?" (toSnippetParams (1 :: Int64, 2 :: Int64, 3 :: Int64)))+ `shouldBe` "WHERE a = $1 AND b = $2 AND c = $3"++ it "should parameterize with :. concatenation" do+ snippetToSql (sqlToSnippet "SELECT * FROM t LIMIT ? OFFSET ?" (toSnippetParams (Only (50 :: Int64) :. Only (100 :: Int64))))+ `shouldBe` "SELECT * FROM t LIMIT $1 OFFSET $2"++ describe "Id' with non-UUID primary keys" do+ it "should encode Text-keyed Id' as a snippet param" do+ let countryId :: Id' "countries" = Id "US"+ snippetToSql (sqlToSnippet "SELECT * FROM countries WHERE id = ?" [Snippet.param countryId])+ `shouldBe` "SELECT * FROM countries WHERE id = $1"++ it "should encode Int-keyed Id' as a snippet param" do+ let serialId :: Id' "serial_table" = Id 42+ snippetToSql (sqlToSnippet "SELECT * FROM serial_table WHERE id = ?" [Snippet.param serialId])+ `shouldBe` "SELECT * FROM serial_table WHERE id = $1"++ it "should encode UUID Id' as a snippet param (regression)" do+ let userId :: Id' "users" = Id "550e8400-e29b-41d4-a716-446655440000"+ snippetToSql (sqlToSnippet "SELECT * FROM users WHERE id = ?" [Snippet.param userId])+ `shouldBe` "SELECT * FROM users WHERE id = $1"++ it "should encode Maybe (Id' with Text key) as a snippet param" do+ let countryId :: Maybe (Id' "countries") = Just (Id "US")+ snippetToSql (sqlToSnippet "SELECT * FROM t WHERE country_id = ?" [Snippet.param countryId])+ `shouldBe` "SELECT * FROM t WHERE country_id = $1"++ it "should encode [Id' with Text key] as a snippet param" do+ let countryIds :: [Id' "countries"] = [Id "US", Id "DE"]+ snippetToSql (sqlToSnippet "SELECT * FROM countries WHERE id = ANY(?)" [Snippet.param countryIds])+ `shouldBe` "SELECT * FROM countries WHERE id = ANY($1)"
@@ -0,0 +1,148 @@+module Test.JobQueueSpec where++import Test.Hspec+import IHP.Prelude+import qualified IHP.Job.Queue as JobQueue+import IHP.ModelSupport (createModelContext, releaseModelContext, HasqlError (..))+import qualified IHP.Log as Log+import IHP.Log.Types (Logger, LogLevel (..), LoggerSettings (..))+import qualified IHP.PGListener as PGListener+import qualified Hasql.Pool as HasqlPool+import qualified Hasql.Session as HasqlSession+import Control.Monad.Trans.Resource (runResourceT)+import Control.Concurrent.STM (atomically, newTBQueue)+import qualified Control.Concurrent as Concurrent+import qualified Control.Exception as Exception+import System.Environment (lookupEnv)++data TestContext = TestContext+ { logger :: Logger+ }++tests :: Spec+tests = do+ describe "IHP.Job.Queue" do+ it "recreates missing triggers when poller repair is enabled" do+ withJobWatcher True \pool -> do+ dropNotificationTriggers pool testTableName+ didRecover <- waitUntil 6_000_000 (JobQueue.notificationTriggersHealthy pool testTableName)+ didRecover `shouldBe` True++ it "does not recreate missing triggers when poller repair is disabled" do+ withJobWatcher False \pool -> do+ dropNotificationTriggers pool testTableName+ Concurrent.threadDelay 3_000_000+ healthy <- JobQueue.notificationTriggersHealthy pool testTableName+ healthy `shouldBe` False++ it "keeps polling after trigger repair errors and recovers once table exists again" do+ withJobWatcher True \pool -> do+ dropTestTable pool testTableName+ Concurrent.threadDelay 1_500_000+ createTestTable pool testTableName+ didRecover <- waitUntil 6_000_000 (JobQueue.notificationTriggersHealthy pool testTableName)+ didRecover `shouldBe` True++ it "recreates missing triggers for long table names when poller repair is enabled" do+ withJobWatcherForTable True longTestTableName \pool -> do+ dropNotificationTriggers pool longTestTableName+ didRecover <- waitUntil 6_000_000 (JobQueue.notificationTriggersHealthy pool longTestTableName)+ didRecover `shouldBe` True++withJobWatcher :: Bool -> (HasqlPool.Pool -> IO ()) -> IO ()+withJobWatcher enablePollerTriggerRepair =+ withJobWatcherForTable enablePollerTriggerRepair testTableName++withJobWatcherForTable :: Bool -> Text -> (HasqlPool.Pool -> IO ()) -> IO ()+withJobWatcherForTable enablePollerTriggerRepair tableName action = do+ withDB \modelContext logger databaseUrl -> do+ let ?context = TestContext { logger = logger }+ let pool = modelContext.hasqlPool++ Exception.finally+ (do+ dropTestArtifacts pool tableName+ createTestTable pool tableName++ PGListener.withPGListener databaseUrl logger \pgListener -> do+ runResourceT do+ queue <- liftIO (atomically (newTBQueue 32))+ (subscription, _) <- JobQueue.watchForJobWithPollerTriggerRepair enablePollerTriggerRepair pool pgListener tableName 100000 queue+ liftIO (action pool `Exception.finally` PGListener.unsubscribe subscription pgListener))+ (dropTestArtifacts pool tableName)++withDB :: (ModelContext -> Logger -> ByteString -> IO ()) -> IO ()+withDB action = do+ envUrl <- lookupEnv "DATABASE_URL"+ let databaseUrl = maybe "postgresql:///postgres" cs envUrl+ logger <- Log.newLogger def { level = Warn }+ modelContext <- createModelContext databaseUrl logger+ result <- Exception.try (action modelContext logger databaseUrl `Exception.finally` releaseModelContext modelContext)+ case result of+ Right () -> pure ()+ Left (HasqlError (HasqlPool.ConnectionUsageError _)) ->+ pendingWith "PostgreSQL not available (set DATABASE_URL or start a local Postgres)"+ Left e -> Exception.throwIO e++testTableName :: Text+testTableName = "job_queue_spec_jobs"++longTestTableName :: Text+longTestTableName = "job_queue_spec_jobs_with_a_very_long_table_name_for_trigger_truncation_regression_1234567890"++insertTriggerName :: Text -> Text+insertTriggerName tableName = "did_insert_job_" <> tableName++updateTriggerName :: Text -> Text+updateTriggerName tableName = "did_update_job_" <> tableName++triggerFunctionName :: Text -> Text+triggerFunctionName tableName = "notify_job_queued_" <> tableName++createTestTable :: HasqlPool.Pool -> Text -> IO ()+createTestTable pool tableName = do+ runScript pool $+ "CREATE TABLE IF NOT EXISTS \"" <> tableName <> "\" ("+ <> " id UUID PRIMARY KEY,"+ <> " status TEXT DEFAULT 'job_status_not_started' NOT NULL,"+ <> " locked_by UUID DEFAULT NULL,"+ <> " run_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,"+ <> " created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"+ <> " );"++dropTestTable :: HasqlPool.Pool -> Text -> IO ()+dropTestTable pool tableName =+ runScript pool ("DROP TABLE IF EXISTS \"" <> tableName <> "\" CASCADE;")++dropNotificationTriggers :: HasqlPool.Pool -> Text -> IO ()+dropNotificationTriggers pool tableName =+ runScript pool $+ "DROP TRIGGER IF EXISTS " <> insertTriggerName tableName <> " ON \"" <> tableName <> "\";"+ <> "DROP TRIGGER IF EXISTS " <> updateTriggerName tableName <> " ON \"" <> tableName <> "\";"++dropTestArtifacts :: HasqlPool.Pool -> Text -> IO ()+dropTestArtifacts pool tableName =+ runScript pool $+ "DROP TABLE IF EXISTS \"" <> tableName <> "\" CASCADE;"+ <> "DROP FUNCTION IF EXISTS " <> triggerFunctionName tableName <> "() CASCADE;"++runScript :: HasqlPool.Pool -> Text -> IO ()+runScript pool sql = do+ result <- HasqlPool.use pool (HasqlSession.script sql)+ case result of+ Left err -> Exception.throwIO (HasqlError err)+ Right () -> pure ()++waitUntil :: Int -> IO Bool -> IO Bool+waitUntil timeoutInMicroseconds predicate = loop 0+ where+ interval = 100000+ loop elapsed+ | elapsed >= timeoutInMicroseconds = pure False+ | otherwise = do+ value <- predicate+ if value+ then pure True+ else do+ Concurrent.threadDelay interval+ loop (elapsed + interval)
@@ -0,0 +1,54 @@+module Main where++import Test.Hspec+import IHP.Prelude++import qualified Test.ValidationSupport.ValidateFieldSpec+import qualified Test.NameSupportSpec+import qualified Test.HaskellSupportSpec+import qualified Test.View.CSSFrameworkSpec+import qualified Test.View.FormSpec+import qualified Test.Controller.ContextSpec+import qualified Test.Controller.ParamSpec+import qualified Test.Controller.CookieSpec+import qualified Test.Controller.AccessDeniedSpec+import qualified Test.Controller.NotFoundSpec+import qualified Test.ModelSupportSpec+import qualified Test.QueryBuilderSpec+import qualified Test.RouterSupportSpec+import qualified Test.ViewSupportSpec+import qualified Test.FileStorage.ControllerFunctionsSpec+import qualified Test.PGListenerSpec+import qualified Test.MockingSpec+import qualified Test.HasqlEncoderSpec+import qualified Test.ControllerSupportSpec+import qualified Test.AutoRefreshSpec+import qualified Test.Pagination.ControllerFunctionsSpec+import qualified Test.FetchPipelinedSpec+import qualified Test.JobQueueSpec++main :: IO ()+main = hspec do+ Test.ValidationSupport.ValidateFieldSpec.tests+ Test.NameSupportSpec.tests+ Test.HaskellSupportSpec.tests+ Test.View.CSSFrameworkSpec.tests+ Test.View.FormSpec.tests+ Test.Controller.ContextSpec.tests+ Test.Controller.ParamSpec.tests+ Test.Controller.AccessDeniedSpec.tests+ Test.Controller.NotFoundSpec.tests+ Test.ModelSupportSpec.tests+ Test.QueryBuilderSpec.tests+ Test.RouterSupportSpec.tests+ Test.ViewSupportSpec.tests+ Test.FileStorage.ControllerFunctionsSpec.tests+ Test.Controller.CookieSpec.tests+ Test.PGListenerSpec.tests+ Test.MockingSpec.tests+ Test.HasqlEncoderSpec.tests+ Test.ControllerSupportSpec.tests+ Test.AutoRefreshSpec.tests+ Test.Pagination.ControllerFunctionsSpec.tests+ Test.FetchPipelinedSpec.tests+ Test.JobQueueSpec.tests
@@ -0,0 +1,70 @@+{-|+Module: Test.MockingSpec+Tests for IHP.Test.Mocking, ensuring callActionWithParams correctly+delivers params through the middleware stack and that redirect responses+preserve their status codes.+-}+{-# LANGUAGE AllowAmbiguousTypes #-}++module Test.MockingSpec where+import ClassyPrelude+import Test.Hspec+import IHP.Test.Mocking+import IHP.Prelude+import IHP.Environment+import IHP.FrameworkConfig+import IHP.ControllerPrelude hiding (get, request)+import Text.Blaze.Html (Html)+import Network.Wai+import Network.HTTP.Types++data WebApplication = WebApplication deriving (Eq, Show, Data)++data TestController+ = EchoParamAction+ | RedirectAction+ deriving (Eq, Show, Data)++instance Controller TestController where+ action EchoParamAction = do+ let value = param @Text "message"+ renderPlain (cs value)+ action RedirectAction = do+ redirectToPath "/target"++instance AutoRoute TestController++instance FrontController WebApplication where+ controllers = [ parseRoute @TestController ]++defaultLayout :: Html -> Html+defaultLayout inner = inner++instance InitControllerContext WebApplication where+ initContext = do+ setLayout defaultLayout++instance FrontController RootApplication where+ controllers = [ mountFrontController WebApplication ]++config = do+ option Development+ option (AppPort 8000)++tests :: Spec+tests = beforeAll (mockContextNoDatabase WebApplication config) do+ describe "IHP.Test.Mocking" do+ describe "callActionWithParams" do+ it "should deliver params to the controller action" $ withContext do+ response <- callActionWithParams EchoParamAction [("message", "hello world")]+ body <- responseBody response+ cs body `shouldBe` ("hello world" :: Text)++ it "should return status 200 for rendered responses" $ withContext do+ response <- callActionWithParams EchoParamAction [("message", "test")]+ responseStatus response `shouldBe` status200++ describe "redirectTo" do+ it "should return status 302" $ withContext do+ response <- callAction RedirectAction+ responseStatus response `shouldBe` status302
@@ -0,0 +1,146 @@+{-|+Module: Test.ModelFixtures+Description: Shared model types for test specs+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.ModelFixtures where++import IHP.Prelude+import IHP.ModelSupport+import IHP.QueryBuilder (FilterPrimaryKey(..), filterWhere)+import IHP.Hasql.FromRow (FromRowHasql(..), HasqlDecodeColumn(..))+import IHP.Job.Types (JobStatus(..))+import IHP.Job.Queue ()++data Post = Post+ { id :: UUID+ , title :: Text+ , externalUrl :: Maybe Text+ , createdAt :: UTCTime+ , public :: Bool+ , createdBy :: UUID+ , categoryId :: Maybe UUID+ }++type instance GetTableName Post = "posts"+type instance GetModelByTableName "posts" = Post+type instance PrimaryKey "posts" = UUID++instance Table Post where+ columnNames = ["id", "title", "external_url", "created_at", "public", "created_by", "category_id"]+ primaryKeyColumnNames = ["id"]++instance FromRowHasql Post where+ hasqlRowDecoder = Post+ <$> hasqlColumnDecoder+ <*> hasqlColumnDecoder+ <*> hasqlColumnDecoder+ <*> hasqlColumnDecoder+ <*> hasqlColumnDecoder+ <*> hasqlColumnDecoder+ <*> hasqlColumnDecoder++data WeirdPkTag = WeirdPkTag+ { tagIden :: UUID+ , tagText :: Text+ }++type instance GetTableName WeirdPkTag = "weird_tags"+type instance GetModelByTableName "weird_tags" = WeirdPkTag+type instance PrimaryKey "weird_tags" = UUID++instance Table WeirdPkTag where+ columnNames = ["tag_iden", "tag_text"]+ primaryKeyColumnNames = ["tag_iden"]++data Tag = Tag+ { id :: UUID+ , tagText :: Text+ }++type instance GetTableName Tag = "tags"+type instance GetModelByTableName "tags" = Tag+type instance PrimaryKey "tags" = UUID++instance Table Tag where+ columnNames = ["id", "tag_text"]+ primaryKeyColumnNames = ["id"]++data Tagging = Tagging+ { id :: UUID+ , postId :: UUID+ , tagId :: UUID+ }++type instance GetTableName Tagging = "taggings"+type instance GetModelByTableName "taggings" = Tagging+type instance PrimaryKey "taggings" = UUID++instance Table Tagging where+ columnNames = ["id", "post_id", "tag_id"]+ primaryKeyColumnNames = ["id"]++data CompositeTagging = CompositeTagging+ { postId :: UUID+ , tagId :: UUID+ }++type instance GetTableName CompositeTagging = "composite_taggings"+type instance GetModelByTableName "composite_taggings" = CompositeTagging+type instance PrimaryKey "composite_taggings" = (Id' "posts", Id' "tags")++instance Table CompositeTagging where+ columnNames = ["post_id", "tag_id"]+ primaryKeyColumnNames = ["post_id", "tag_id"]++instance FromRowHasql CompositeTagging where+ hasqlRowDecoder = CompositeTagging+ <$> hasqlColumnDecoder+ <*> hasqlColumnDecoder++instance FilterPrimaryKey "composite_taggings" where+ filterWhereId (Id (Id postId, Id tagId)) builder =+ builder |> filterWhere (#postId, postId) |> filterWhere (#tagId, tagId)++data User = User+ { id :: UUID+ , name :: Text+ }++type instance GetTableName User = "users"+type instance GetModelByTableName "users" = User+type instance PrimaryKey "users" = UUID++instance Table User where+ columnNames = ["id", "name"]+ primaryKeyColumnNames = ["id"]++instance FromRowHasql User where+ hasqlRowDecoder = User+ <$> hasqlColumnDecoder+ <*> hasqlColumnDecoder++data FavoriteTitle = FavoriteTitle+ { title :: Text+ , likes :: Int+ }++type instance GetTableName FavoriteTitle = "favorite_title"+type instance GetModelByTableName "favorite_title" = FavoriteTitle++instance Table FavoriteTitle where+ columnNames = ["title", "likes"]+ primaryKeyColumnNames = []++data BackgroundJob = BackgroundJob+ { id :: UUID+ , status :: JobStatus+ }++type instance GetTableName BackgroundJob = "background_jobs"+type instance GetModelByTableName "background_jobs" = BackgroundJob+type instance PrimaryKey "background_jobs" = UUID++instance Table BackgroundJob where+ columnNames = ["id", "status"]+ primaryKeyColumnNames = ["id"]
@@ -0,0 +1,141 @@+{-|+Module: Test.HaskellSupportSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.ModelSupportSpec where++import Test.Hspec+import IHP.Prelude+import IHP.ModelSupport+import Data.Bits ((.&.), (.|.))+import qualified Data.Aeson as Aeson+import qualified Data.Dynamic as Dynamic+import Text.Read (read)+import Data.Scientific (Scientific)++tests = do+ describe "ModelSupport" do+ describe "InputValue" do+ describe "Text" do+ it "should return the text" do+ let text :: Text = "Lorem Ipsum"+ (inputValue text) `shouldBe` text++ describe "Int" do+ it "should return numeric representation" do+ (inputValue (1 :: Int)) `shouldBe` "1"++ describe "Integer" do+ it "should return numeric representation" do+ (inputValue (1 :: Int)) `shouldBe` "1"++ describe "Double" do+ it "should return numeric representation" do+ (inputValue (1.337 :: Double)) `shouldBe` "1.337"+ + it "should deal with large numbers" do+ -- https://stackoverflow.com/questions/69306646/ihp-haskell-field-display-format-double-values+ (inputValue (50000000 :: Double)) `shouldBe` "50000000.0"++ describe "Scientific" do+ it "should return numeric representation" do+ (inputValue (1.0e-1024 :: Scientific)) `shouldBe` "1.0e-1024"+ -- -1024 is smaller than minimal Double exponent of -1021++ describe "Float" do+ it "should return numeric representation" do+ (inputValue (1.337 :: Float)) `shouldBe` "1.337"+ + it "should deal with large numbers" do+ -- https://stackoverflow.com/questions/69306646/ihp-haskell-field-display-format-double-values+ (inputValue (50000000 :: Float)) `shouldBe` "50000000.0"++ describe "Bool" do+ it "should deal with True and False" do+ (inputValue True) `shouldBe` "on"+ (inputValue False) `shouldBe` "off"++ describe "UUID" do+ it "should return text representation" do+ let uuid :: UUID = "b9a1129e-e53a-4370-b778-0d1c28dc6ecc"+ (inputValue uuid) `shouldBe` "b9a1129e-e53a-4370-b778-0d1c28dc6ecc"++ describe "UTCTime" do+ it "should return text representation" do+ let (Just utctime) :: Maybe UTCTime = parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" "2020-11-08T12:03:35Z"+ (inputValue utctime) `shouldBe` "2020-11-08T12:03:35Z"++ describe "Day" do+ it "should return text representation" do+ let (Just utctime) :: Maybe UTCTime = parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" "2020-11-08T12:03:35Z"+ let day :: Day = utctDay utctime+ (inputValue day) `shouldBe` "2020-11-08"++ describe "TimeOfDay" do+ it "should return text representation" do+ let timeOfDay :: TimeOfDay = read "12:00:00"+ (inputValue timeOfDay) `shouldBe` "12:00:00"++ describe "Maybe" do+ it "should return empty string on Nothing" do+ let value :: Maybe Int = Nothing+ (inputValue value) `shouldBe` ""++ it "should return the value if Just" do+ let value :: Maybe Int = Just 1+ (inputValue value) `shouldBe` "1"++ describe "[a]" do+ it "should return CSV" do+ let value :: [Int] = [1, 2, 3]+ (inputValue value) `shouldBe` "1,2,3"++ it "should return empty string on empty list" do+ let value :: [Int] = []+ (inputValue value) `shouldBe` ""++ describe "JSON" do+ it "should return a json string for a Aeson.Value" do+ let (Just value) :: Maybe Aeson.Value = Aeson.decode "{\"hello\":true}"+ (inputValue value) `shouldBe` "{\"hello\":true}"++ describe "didChange" do+ let project = Project { id = 1337, name = "Test", meta = def { originalDatabaseRecord = Just (Dynamic.toDyn project) } }++ it "should return False for a new record" do+ (project |> didChange #name) `shouldBe` False+ (project |> didChange #id) `shouldBe` False++ it "should return True for a changed field" do+ (project |> set #name "Changed" |> didChange #name) `shouldBe` True+ (project |> didChange #id) `shouldBe` False++ it "should return false for a changed field when set with the same value again" do+ (project |> set #name "Test" |> didChange #name) `shouldBe` False+ (project |> didChange #id) `shouldBe` False++ describe "copyRecord" do+ it "should mark all fields except id as touched" do+ let project = Project { id = 1, name = "Test", meta = def }+ let copied = copyRecord project+ copied.id `shouldBe` 0 -- reset to Default+ copied.name `shouldBe` "Test" -- preserved+ -- id bit (1) should NOT be set, name bit (2) should be set+ copied.meta.touchedFields .&. 1 `shouldBe` 0+ copied.meta.touchedFields .&. 2 `shouldBe` 2+++data Project = Project { id :: Int, name :: Text, meta :: MetaBag }+instance SetField "id" Project Int where+ setField value project@(Project { id, name, meta }) = project { Test.ModelSupportSpec.id = value, meta = meta { touchedFields = touchedFields meta .|. 1 } }+instance SetField "name" Project Text where+ setField value project@(Project { id, name, meta }) = project { name = value, meta = meta { touchedFields = touchedFields meta .|. 2 } }+instance SetField "meta" Project MetaBag where+ setField value project = project { meta = value }+instance FieldBit "id" Project where fieldBit = 1+instance FieldBit "name" Project where fieldBit = 2++type instance GetTableName Project = "projects"+instance Table Project where+ columnNames = ["id", "name"]+ primaryKeyColumnNames = ["id"]
@@ -0,0 +1,115 @@+{-|+Module: Test.NameSupportSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.NameSupportSpec where++import Test.Hspec+import IHP.Prelude+++tests = do+ describe "NameSupport" do+ describe "tableNameToModelName" do+ it "should deal with empty input" do+ tableNameToModelName "" `shouldBe` ""++ it "should transform table names to model names" do+ tableNameToModelName "users" `shouldBe` "User"+ tableNameToModelName "projects" `shouldBe` "Project"+ tableNameToModelName "user_projects" `shouldBe` "UserProject"+ tableNameToModelName "users_projects" `shouldBe` "UsersProject"+ tableNameToModelName "people" `shouldBe` "Person"++ describe "tableNameToControllerName" do+ it "should deal with empty input" do+ tableNameToControllerName "" `shouldBe` ""++ it "should transform table names to controller names" do+ tableNameToControllerName "users" `shouldBe` "Users"+ tableNameToControllerName "projects" `shouldBe` "Projects"+ tableNameToControllerName "user_projects" `shouldBe` "UserProjects"+ tableNameToControllerName "users_projects" `shouldBe` "UsersProjects"+ tableNameToControllerName "people" `shouldBe` "People"++ describe "tableNameToViewName" do+ it "should deal with empty input" do+ tableNameToViewName "" `shouldBe` ""++ it "should transform table names to controller names" do+ tableNameToViewName "users" `shouldBe` "Users"+ tableNameToViewName "projects" `shouldBe` "Projects"+ tableNameToViewName "user_projects" `shouldBe` "UserProjects"+ tableNameToViewName "users_projects" `shouldBe` "UsersProjects"+ tableNameToViewName "people" `shouldBe` "People"++ describe "enumValueToControllerName" do+ it "should handle spaces in table names" do+ enumValueToControllerName "very happy" `shouldBe` "VeryHappy"+ enumValueToControllerName "sad" `shouldBe` "Sad"+ enumValueToControllerName "very sad" `shouldBe` "VerySad"++ it "should deal with typical enum cases" do+ enumValueToControllerName "job_status_not_started" `shouldBe` "JobStatusNotStarted"+ enumValueToControllerName "job_status_running" `shouldBe` "JobStatusRunning"+ enumValueToControllerName "job_status_failed" `shouldBe` "JobStatusFailed"+ enumValueToControllerName "job_status_succeeded" `shouldBe` "JobStatusSucceeded"+ enumValueToControllerName "job_status_retry" `shouldBe` "JobStatusRetry"++ describe "modelNameToTableName" do+ it "should deal with empty input" do+ modelNameToTableName "" `shouldBe` ""++ it "should transform model names to table names" do+ modelNameToTableName "User" `shouldBe` "users"+ modelNameToTableName "Project" `shouldBe` "projects"+ modelNameToTableName "UserProject" `shouldBe` "user_projects"+ modelNameToTableName "UsersProjects" `shouldBe` "users_projects"+ modelNameToTableName "Person" `shouldBe` "people"++ describe "columnNameToFieldName" do+ it "should deal with empty input" do+ columnNameToFieldName "" `shouldBe` ""++ it "should transform column names to field names" do+ columnNameToFieldName "email" `shouldBe` "email"+ columnNameToFieldName "project_id" `shouldBe` "projectId"+ columnNameToFieldName "user_project_name" `shouldBe` "userProjectName"++ describe "fieldNameToColumnName" do+ it "should deal with empty input" do+ fieldNameToColumnName "" `shouldBe` ""++ it "should transform field names to column names" do+ fieldNameToColumnName "email" `shouldBe` "email"+ fieldNameToColumnName "projectId" `shouldBe` "project_id"+ fieldNameToColumnName "userProjectName" `shouldBe` "user_project_name"++ describe "lcfirst" do+ it "should deal with empty input" do+ lcfirst "" `shouldBe` ""++ it "should lowercase the first letter" do+ lcfirst "Hello" `shouldBe` "hello"+ lcfirst "Hello World" `shouldBe` "hello World"+ lcfirst "1337" `shouldBe` "1337"++ describe "ucfirst" do+ it "should deal with empty input" do+ ucfirst "" `shouldBe` ""++ it "should uppercase the first letter" do+ ucfirst "hello" `shouldBe` "Hello"+ ucfirst "hello world" `shouldBe` "Hello world"+ ucfirst "1337" `shouldBe` "1337"++ describe "escapeHaskellKeyword" do+ it "should deal with empty input" do+ escapeHaskellKeyword "" `shouldBe` ""++ it "should escape haskell keywords" do+ escapeHaskellKeyword "type" `shouldBe` "type_"+ escapeHaskellKeyword "DATA" `shouldBe` "DATA_"++ it "should ignore non-haskell keywords" do+ escapeHaskellKeyword "hello" `shouldBe` "hello"
@@ -0,0 +1,45 @@+{-|+Module: Test.PGListenerSpec+Copyright: (c) digitally induced GmbH, 2022+-}+module Test.PGListenerSpec where++import Test.Hspec+import IHP.Prelude+import qualified IHP.PGListener as PGListener+import IHP.Log.Types (Logger(..), LogLevel(..))+import Data.HashMap.Strict as HashMap++tests = do+ describe "IHP.PGListener" do+ let logger = Logger+ { write = \_ -> pure ()+ , level = Debug+ , formatter = \_ _ msg -> msg+ , timeCache = pure ""+ , cleanup = pure ()+ }++ describe "subscribe" do+ it "should add a subscriber" do+ PGListener.withPGListener "" logger \pgListener -> do+ subscriptionsCount <- length . concat . HashMap.elems <$> readIORef pgListener.subscriptions+ subscriptionsCount `shouldBe` 0++ let didInsertRecordCallback notification = pure ()++ pgListener |> PGListener.subscribe "did_insert_record" didInsertRecordCallback++ subscriptionsCount <- length . concat . HashMap.elems <$> readIORef pgListener.subscriptions+ subscriptionsCount `shouldBe` 1++ describe "unsubscribe" do+ it "remove the subscription" do+ PGListener.withPGListener "" logger \pgListener -> do+ subscription <- pgListener |> PGListener.subscribe "did_insert_record" (const (pure ()))+ pgListener |> PGListener.unsubscribe subscription++ subscriptions <- readIORef pgListener.subscriptions+ subscriptionsCount <- length . concat . HashMap.elems <$> readIORef pgListener.subscriptions++ subscriptionsCount `shouldBe` 0
@@ -0,0 +1,160 @@+module Test.Pagination.ControllerFunctionsSpec where++import IHP.Prelude+import Test.Hspec+import IHP.Pagination.ControllerFunctions+import IHP.Pagination.Types (Options(..), Pagination(..))+import IHP.Controller.Context+import IHP.ModelSupport (createModelContext, releaseModelContext, HasqlError(..))+import qualified Hasql.Pool as HasqlPool+import qualified IHP.Log as Log+import IHP.Log.Types (LogLevel(..), LoggerSettings(..))+import Wai.Request.Params.Middleware (RequestBody (..), requestBodyVaultKey)+import qualified Data.Vault.Lazy as Vault+import qualified Data.TMap as TypeMap+import qualified Network.Wai as Wai+import qualified Database.PostgreSQL.Simple.Types as PG+import System.Environment (lookupEnv)+import qualified Control.Exception as Exception++tests :: Spec+tests = do+ describe "IHP.Pagination.ControllerFunctions" do+ describe "paginatedSqlQueryWithOptions" do+ it "should return first page with default options" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ let ?context = contextWithParams []+ let ?request = ?context.request++ (results :: [PG.Only Int32], pagination) <-+ paginatedSqlQueryWithOptions+ defaultPaginationOptions+ "SELECT generate_series(1, 100) AS n"+ ()++ length results `shouldBe` 50+ pagination.currentPage `shouldBe` 1+ pagination.totalItems `shouldBe` 100+ pagination.pageSize `shouldBe` 50++ it "should return second page" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ let ?context = contextWithParams [("page", "2")]+ let ?request = ?context.request++ (results :: [PG.Only Int32], pagination) <-+ paginatedSqlQueryWithOptions+ defaultPaginationOptions+ "SELECT generate_series(1, 100) AS n"+ ()++ length results `shouldBe` 50+ pagination.currentPage `shouldBe` 2+ pagination.totalItems `shouldBe` 100+ -- Second page should have items 51-100+ case results of+ (PG.Only first : _) -> first `shouldBe` 51+ _ -> expectationFailure "Expected non-empty results"++ it "should respect maxItems from request param" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ let ?context = contextWithParams [("maxItems", "10")]+ let ?request = ?context.request++ (results :: [PG.Only Int32], pagination) <-+ paginatedSqlQueryWithOptions+ defaultPaginationOptions+ "SELECT generate_series(1, 100) AS n"+ ()++ length results `shouldBe` 10+ pagination.pageSize `shouldBe` 10+ pagination.totalItems `shouldBe` 100++ it "should respect custom options maxItems" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ let ?context = contextWithParams []+ let ?request = ?context.request+ let options = Options { maxItems = 25, windowSize = 3 }++ (results :: [PG.Only Int32], pagination) <-+ paginatedSqlQueryWithOptions+ options+ "SELECT generate_series(1, 100) AS n"+ ()++ length results `shouldBe` 25+ pagination.pageSize `shouldBe` 25+ pagination.window `shouldBe` 3++ it "should cap maxItems at 200" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ let ?context = contextWithParams [("maxItems", "9999")]+ let ?request = ?context.request++ (results :: [PG.Only Int32], pagination) <-+ paginatedSqlQueryWithOptions+ defaultPaginationOptions+ "SELECT generate_series(1, 500) AS n"+ ()++ length results `shouldBe` 200+ pagination.pageSize `shouldBe` 200+ pagination.totalItems `shouldBe` 500++ it "should return empty results for page beyond data" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ let ?context = contextWithParams [("page", "100")]+ let ?request = ?context.request++ (results :: [PG.Only Int32], pagination) <-+ paginatedSqlQueryWithOptions+ defaultPaginationOptions+ "SELECT generate_series(1, 10) AS n"+ ()++ length results `shouldBe` 0+ pagination.totalItems `shouldBe` 10+ pagination.currentPage `shouldBe` 100++ it "should handle page + maxItems together" $ withDB \modelContext -> do+ let ?modelContext = modelContext+ let ?context = contextWithParams [("page", "3"), ("maxItems", "10")]+ let ?request = ?context.request++ (results :: [PG.Only Int32], pagination) <-+ paginatedSqlQueryWithOptions+ defaultPaginationOptions+ "SELECT generate_series(1, 100) AS n"+ ()++ length results `shouldBe` 10+ pagination.currentPage `shouldBe` 3+ pagination.pageSize `shouldBe` 10+ -- Page 3 with pageSize 10 should start at item 21+ case results of+ (PG.Only first : _) -> first `shouldBe` 21+ _ -> expectationFailure "Expected non-empty results"++-- | Create a ControllerContext with the given request params+contextWithParams :: [(ByteString, ByteString)] -> ControllerContext+contextWithParams params =+ let requestBody = FormBody { params, files = [], rawPayload = "" }+ request = Wai.defaultRequest { Wai.vault = Vault.insert requestBodyVaultKey requestBody Vault.empty }+ customFields = TypeMap.insert request TypeMap.empty+ in FrozenControllerContext { customFields }++-- | Run a test with a database connection, skipping if PostgreSQL is not available.+-- Only connection failures are caught and marked as pending; test assertion errors propagate normally.+withDB :: (ModelContext -> IO ()) -> IO ()+withDB action = do+ envUrl <- lookupEnv "DATABASE_URL"+ let databaseUrl = maybe "postgresql:///postgres" cs envUrl+ logger <- Log.newLogger def { level = Warn }+ modelContext <- createModelContext databaseUrl logger+ result <- Exception.try (action modelContext `Exception.finally` releaseModelContext modelContext)+ case result of+ Right () -> pure ()+ Left (HasqlError (HasqlPool.ConnectionUsageError _)) ->+ pendingWith "PostgreSQL not available"+ Left e -> Exception.throwIO e
@@ -0,0 +1,535 @@+{-|+Module: Test.QueryBuilderSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.QueryBuilderSpec where++import Test.Hspec+import IHP.Prelude+import IHP.QueryBuilder+import IHP.ModelSupport+import IHP.Job.Types (JobStatus(..))+import IHP.Job.Queue ()+import Test.ModelFixtures++tests = do+ describe "QueryBuilder" do+ let postColumns = "posts.id, posts.title, posts.external_url, posts.created_at, posts.public, posts.created_by, posts.category_id"++ describe "query" do+ it "should provide a simple sql query" do+ let theQuery = query @Post++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts")++ describe "filterWhere" do+ it "should produce a SQL with a WHERE condition" do+ let theQuery = query @Post+ |> filterWhere (#title, "Test" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.title = $1")++ it "should use IS operator for checking null" do+ let theQuery = query @Post+ |> filterWhere (#externalUrl, Nothing)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.external_url IS NULL")++ it "should not consume a parameter slot for IS NULL" do+ let theQuery = query @Post+ |> filterWhere (#externalUrl, Nothing)+ |> filterWhere (#title, "Test" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE (posts.external_url IS NULL) AND (posts.title = $1)")++ describe "filterWhereNot" do+ it "should produce a SQL with a WHERE NOT condition" do+ let theQuery = query @Post+ |> filterWhereNot (#title, "Test" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.title != $1")++ it "should use IS NOT operator for checking null" do+ let theQuery = query @Post+ |> filterWhereNot (#externalUrl, Nothing)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.external_url IS NOT NULL")++ it "should not consume a parameter slot for IS NOT NULL" do+ let theQuery = query @Post+ |> filterWhereNot (#externalUrl, Nothing)+ |> filterWhere (#title, "Test" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE (posts.external_url IS NOT NULL) AND (posts.title = $1)")++ describe "filterWhereIn" do+ it "should use = ANY for IN clause" do+ let theValues :: [Text] = ["first", "second"]+ let theQuery = query @Post+ |> filterWhereIn (#title, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.title = ANY ($1)")++ it "should use = ANY for UUID IN clause" do+ let theValues :: [UUID] = ["b80e37a8-41d4-4731-b050-a716879ef1d1", "629b7ee0-3675-4b02-ba3e-cdbd7b513553"]+ let theQuery = query @Post+ |> filterWhereIn (#id, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.id = ANY ($1)")++ describe "with Maybe / NULL values" do+ it "should handle [Just .., Nothing]" do+ let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574", Nothing]+ let theQuery = query @Post+ |> filterWhereIn (#categoryId, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE (posts.category_id = ANY ($1)) OR (posts.category_id IS NULL)")++ it "should handle [Just ..]" do+ let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574"]+ let theQuery = query @Post+ |> filterWhereIn (#categoryId, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.category_id = ANY ($1)")++ it "should handle [Nothing]" do+ let theValues :: [Maybe UUID] = [Nothing]+ let theQuery = query @Post+ |> filterWhereIn (#categoryId, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.category_id IS NULL")++ it "should not consume a parameter slot for IS NULL in mixed list" do+ let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574", Nothing]+ let theQuery = query @Post+ |> filterWhereIn (#categoryId, theValues)+ |> filterWhere (#title, "Test" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE ((posts.category_id = ANY ($1)) OR (posts.category_id IS NULL)) AND (posts.title = $2)")++ describe "filterWhereInCaseInsensitive" do+ it "should produce a SQL with a WHERE LOWER() condition" do+ let theQuery = query @Post+ |> filterWhereInCaseInsensitive (#title, ["Test" :: Text, "Test 1" :: Text])++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE LOWER(posts.title) = ANY ($1)")++ describe "filterWhereNotIn" do+ it "should use <> ALL for NOT IN clause" do+ let theValues :: [Text] = ["first", "second"]+ let theQuery = query @Post+ |> filterWhereNotIn (#title, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.title <> ALL ($1)")++ it "should ignore an empty value list" do+ let theValues :: [Text] = []+ let theQuery = query @Post+ |> filterWhereNotIn (#title, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts")++ describe "with Maybe / NULL values" do+ it "should handle [Just .., Nothing]" do+ let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574", Nothing]+ let theQuery = query @Post+ |> filterWhereNotIn (#categoryId, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE (posts.category_id <> ALL ($1)) AND (posts.category_id IS NOT NULL)")++ it "should handle [Just ..]" do+ let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574"]+ let theQuery = query @Post+ |> filterWhereNotIn (#categoryId, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.category_id <> ALL ($1)")++ it "should handle [Nothing]" do+ let theValues :: [Maybe UUID] = [Nothing]+ let theQuery = query @Post+ |> filterWhereNotIn (#categoryId, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.category_id IS NOT NULL")++ it "should not consume a parameter slot for IS NOT NULL in mixed list" do+ let theValues :: [Maybe UUID] = ["44dcf2cf-a79d-4caf-a2ea-427838ba3574", Nothing]+ let theQuery = query @Post+ |> filterWhereNotIn (#categoryId, theValues)+ |> filterWhere (#title, "Test" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE ((posts.category_id <> ALL ($1)) AND (posts.category_id IS NOT NULL)) AND (posts.title = $2)")++ describe "filterWhereIn with JobStatus" do+ it "should use = ANY for JobStatus IN clause" do+ let jobColumns = "background_jobs.id, background_jobs.status"+ let theValues :: [JobStatus] = [JobStatusSucceeded, JobStatusFailed]+ let theQuery = query @BackgroundJob+ |> filterWhereIn (#status, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> jobColumns <> " FROM background_jobs WHERE background_jobs.status = ANY ($1)")++ describe "filterWhereNotIn with JobStatus" do+ it "should use <> ALL for JobStatus NOT IN clause" do+ let jobColumns = "background_jobs.id, background_jobs.status"+ let theValues :: [JobStatus] = [JobStatusSucceeded, JobStatusFailed]+ let theQuery = query @BackgroundJob+ |> filterWhereNotIn (#status, theValues)++ (toSQL theQuery) `shouldBe` ("SELECT " <> jobColumns <> " FROM background_jobs WHERE background_jobs.status <> ALL ($1)")++ describe "filterWhereIdIn" do+ it "should use = ANY for Id IN clause" do+ let theValues :: [Id Post] = ["b80e37a8-41d4-4731-b050-a716879ef1d1", "629b7ee0-3675-4b02-ba3e-cdbd7b513553"]+ let theQuery = query @Post+ |> filterWhereIdIn theValues++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.id = ANY ($1)")++ describe "with empty values" do+ it "should produce a SQL with a WHERE condition" do+ let theValues :: [Id Post] = []+ let theQuery = query @Post+ |> filterWhereIdIn theValues++ -- Empty list still uses the same pattern but with empty array+ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.id = ANY ($1)")++ describe "with weird primary key name" do+ it "should produce a SQL with a WHERE condition" do+ let theValues :: [Id WeirdPkTag] = ["b80e37a8-41d4-4731-b050-a716879ef1d1", "629b7ee0-3675-4b02-ba3e-cdbd7b513553"]+ let theQuery = query @WeirdPkTag+ |> filterWhereIdIn theValues++ (toSQL theQuery) `shouldBe` "SELECT weird_tags.tag_iden, weird_tags.tag_text FROM weird_tags WHERE weird_tags.tag_iden = ANY ($1)"++ describe "with composite keys" do+ it "should produce a SQL with a WHERE condition" do+ let theValues :: [Id CompositeTagging] = [Id ("b80e37a8-41d4-4731-b050-a716879ef1d1", "629b7ee0-3675-4b02-ba3e-cdbd7b513553"), Id ("8e2ef0ef-f680-4fcf-837d-7e3171385621", "95096f81-8ca6-407f-a263-cbc33546a828")]+ let theQuery = query @CompositeTagging+ |> filterWhereIdIn theValues++ (toSQL theQuery) `shouldBe` "SELECT composite_taggings.post_id, composite_taggings.tag_id FROM composite_taggings WHERE (composite_taggings.post_id, composite_taggings.tag_id) = ANY ($1)"++ describe "with empty values" do+ it "should produce a SQL with a WHERE condition" do+ let theValues :: [Id CompositeTagging] = []+ let theQuery = query @CompositeTagging+ |> filterWhereIdIn theValues++ (toSQL theQuery) `shouldBe` "SELECT composite_taggings.post_id, composite_taggings.tag_id FROM composite_taggings WHERE (composite_taggings.post_id, composite_taggings.tag_id) = ANY ($1)"++ describe "filterWhereId" do+ describe "with composite keys" do+ it "should decompose into per-column WHERE conditions" do+ let theId :: Id CompositeTagging = Id (Id "b80e37a8-41d4-4731-b050-a716879ef1d1", Id "629b7ee0-3675-4b02-ba3e-cdbd7b513553")+ let theQuery = query @CompositeTagging+ |> filterWhereId theId++ (toSQL theQuery) `shouldBe` "SELECT composite_taggings.post_id, composite_taggings.tag_id FROM composite_taggings WHERE (composite_taggings.post_id = $1) AND (composite_taggings.tag_id = $2)"++ describe "filterWhereCaseInsensitive" do+ it "should produce a SQL with a WHERE LOWER() condition" do+ let theQuery = query @Post+ |> filterWhereCaseInsensitive (#title, "Test" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE LOWER(posts.title) = LOWER($1)")++ describe "filterWhereILike" do+ it "should produce a SQL with a WHERE ILIKE condition" do+ let searchTerm = "good"+ let theQuery = query @Post+ |> filterWhereILike (#title, "%" <> searchTerm <> "%")++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.title ILIKE $1")++ describe "filterWhereILikeJoinedTable" do+ it "should produce a SQL with a WHERE condition" do+ let searchTerm = "louis"+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> filterWhereILikeJoinedTable @User (#name, "%" <> searchTerm <> "%")++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id WHERE users.name ILIKE $1")++ describe "filterWhereInJoinedTable" do+ it "should produce a SQL with a WHERE condition" do+ let theValues :: [Text] = ["first", "second"]+ let theQuery = query @User+ |> innerJoin @Post (#name, #title)+ |> filterWhereInJoinedTable @Post (#title, theValues)++ (toSQL theQuery) `shouldBe` "SELECT users.id, users.name FROM users INNER JOIN posts ON users.name = posts.title WHERE posts.title = ANY ($1)"++ describe "filterWhereNotInJoinedTable" do+ it "should produce a SQL with a WHERE condition" do+ let theValues :: [Text] = ["first", "second"]+ let theQuery = query @User+ |> innerJoin @Post (#name, #title)+ |> filterWhereNotInJoinedTable @Post (#title, theValues)++ (toSQL theQuery) `shouldBe` "SELECT users.id, users.name FROM users INNER JOIN posts ON users.name = posts.title WHERE posts.title <> ALL ($1)"++ it "should ignore an empty value list" do+ let theValues :: [Text] = []+ let theQuery = query @User+ |> innerJoin @Post (#name, #title)+ |> filterWhereNotInJoinedTable @Post (#title, theValues)++ (toSQL theQuery) `shouldBe` "SELECT users.id, users.name FROM users INNER JOIN posts ON users.name = posts.title"++ describe "filterWherePast" do+ it "should produce a SQL with the correct WHERE condition" do+ let theQuery = query @Post+ |> filterWherePast #createdAt++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.created_at <= NOW()")++ describe "filterWhereFuture" do+ it "should produce a SQL with the correct WHERE condition" do+ let theQuery = query @Post+ |> filterWhereFuture #createdAt++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.created_at > NOW()")++ describe "filterWhereGreaterThan" do+ it "should produce a SQL with a WHERE > condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereGreaterThan (#likes, 100 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes > $1"++ describe "filterWhereLessThan" do+ it "should produce a SQL with a WHERE < condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereLessThan (#likes, 50 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes < $1"++ describe "filterWhereLarger" do+ it "should produce a SQL with a WHERE > condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereLarger (#likes, 100 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes > $1"++ describe "filterWhereSmaller" do+ it "should produce a SQL with a WHERE < condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereSmaller (#likes, 50 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes < $1"++ describe "filterWhereGreaterThanOrEqualTo" do+ it "should produce a SQL with a WHERE >= condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereGreaterThanOrEqualTo (#likes, 80 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes >= $1"++ describe "filterWhereAtLeast" do+ it "should produce a SQL with a WHERE >= condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereAtLeast (#likes, 80 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes >= $1"++ describe "filterWhereLessThanOrEqualTo" do+ it "should produce a SQL with a WHERE <= condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereLessThanOrEqualTo (#likes, 60 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes <= $1"++ describe "filterWhereAtMost" do+ it "should produce a SQL with a WHERE <= condition" do+ let theQuery = query @FavoriteTitle+ |> filterWhereAtMost (#likes, 60 :: Int)++ (toSQL theQuery) `shouldBe` "SELECT favorite_title.title, favorite_title.likes FROM favorite_title WHERE favorite_title.likes <= $1"++ describe "filterWhereJoinedTable" do+ it "should produce a SQL with a WHERE condition on joined table" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> innerJoin @FavoriteTitle (#title, #title)+ |> filterWhereJoinedTable @User (#name, "Tom" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title WHERE users.name = $1")++ describe "filterWhereNotJoinedTable" do+ it "should produce a SQL with a WHERE NOT condition on joined table" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> innerJoin @FavoriteTitle (#title, #title)+ |> filterWhereNotJoinedTable @User (#name, "Tom" :: Text)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title WHERE users.name != $1")++ describe "filterWhereSql" do+ it "should produce a SQL with a raw WHERE condition" do+ let theQuery = query @Post+ |> filterWhereSql (#createdAt, "< current_timestamp - interval '1 day'")++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE posts.created_at < current_timestamp - interval '1 day'")++ describe "queryOr" do+ it "should merge two conditions" do+ let theQuery = query @Post+ |> queryOr+ (filterWhere (#createdBy, "fe41a985-36a3-4f14-b13c-c166977dc7e8" :: UUID))+ (filterWhere (#public, True))++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE (posts.created_by = $1) OR (posts.public = $2)")++ describe "innerJoin" do+ it "should provide an inner join sql query" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> innerJoin @FavoriteTitle (#title, #title)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title")++ describe "innerJoinThirdTable" do+ it "should provide an inner join sql query" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> innerJoin @FavoriteTitle (#title, #title)+ |> innerJoinThirdTable @User @FavoriteTitle (#name, #title)++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id INNER JOIN favorite_title ON posts.title = favorite_title.title INNER JOIN users ON favorite_title.title = users.name")++ describe "labelResults" do+ it "should provide a query with index field" do+ let theQuery = query @Tag+ |> innerJoin @Tagging (#id, #tagId)+ |> innerJoinThirdTable @Post @Tagging (#id, #postId)+ |> labelResults @Post #id+ (toSQL theQuery) `shouldBe` "SELECT posts.id, tags.id, tags.tag_text FROM tags INNER JOIN taggings ON tags.id = taggings.tag_id INNER JOIN posts ON taggings.post_id = posts.id"++ describe "orderBy" do+ describe "orderByAsc" do+ it "should add a ORDER BY" do+ let theQuery = query @Post+ |> orderByAsc #createdAt++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts ORDER BY posts.created_at")++ it "should accumulate multiple ORDER BY's" do+ let theQuery = query @Post+ |> orderByAsc #createdAt+ |> orderByAsc #title++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts ORDER BY posts.created_at,posts.title")++ describe "orderByDesc" do+ it "should add a ORDER BY DESC" do+ let theQuery = query @Post+ |> orderByDesc #createdAt++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts ORDER BY posts.created_at DESC")++ it "should accumulate multiple ORDER BY's" do+ let theQuery = query @Post+ |> orderByDesc #createdAt+ |> orderByDesc #title++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts ORDER BY posts.created_at DESC,posts.title DESC")++ describe "orderByJoinedTable" do+ it "should add a ORDER BY" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> orderByJoinedTable @User #name++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id ORDER BY users.name")++ it "should accumulate multiple ORDER BY's" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> orderByJoinedTable @User #name+ |> orderByJoinedTable @User #id++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id ORDER BY users.name,users.id")++ describe "orderByAscJoinedTable" do+ it "should add a ORDER BY" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> orderByAscJoinedTable @User #name++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id ORDER BY users.name")++ it "should accumulate multiple ORDER BY's" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> orderByAscJoinedTable @User #name+ |> orderByAscJoinedTable @User #id++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id ORDER BY users.name,users.id")++ describe "orderByDescJoinedTable" do+ it "should add a ORDER BY" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> orderByDescJoinedTable @User #name++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id ORDER BY users.name DESC")++ it "should accumulate multiple ORDER BY's" do+ let theQuery = query @Post+ |> innerJoin @User (#createdBy, #id)+ |> orderByDescJoinedTable @User #name+ |> orderByDescJoinedTable @User #id++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts INNER JOIN users ON posts.created_by = users.id ORDER BY users.name DESC,users.id DESC")++ describe "limit" do+ it "should add a LIMIT" do+ let theQuery = query @Post+ |> limit 1337++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts LIMIT $1")++ describe "offset" do+ it "should add an OFFSET" do+ let theQuery = query @Post+ |> offset 1337++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts OFFSET $1")++ describe "distinct" do+ it "should add a DISTINCT" do+ let theQuery = query @Post+ |> distinct++ (toSQL theQuery) `shouldBe` ("SELECT DISTINCT " <> postColumns <> " FROM posts")++ describe "distinctOn" do+ it "should add a DISTINCT ON (..)" do+ let theQuery = query @Post+ |> distinctOn #title++ (toSQL theQuery) `shouldBe` ("SELECT DISTINCT ON (posts.title) " <> postColumns <> " FROM posts")++ describe "Complex Queries" do+ it "should allow a query with limit and offset" do+ let theQuery = query @Post+ |> offset 20+ |> limit 50++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts LIMIT $1 OFFSET $2")++ it "should work with multiple complex conditions" do+ let theQuery = query @Post+ |> queryOr+ (\qb -> qb+ |> filterWhere (#title, "test")+ |> filterWhere (#public, True)+ |> filterWhere (#externalUrl, Nothing)+ )+ (filterWhere (#createdBy, "e70c66fb-68a5-41b8-8bf1-85b9bb046d15" :: UUID))+ |> orderBy #createdAt+ |> orderBy #title+ |> limit 10++ (toSQL theQuery) `shouldBe` ("SELECT " <> postColumns <> " FROM posts WHERE (((posts.title = $1) AND (posts.public = $2)) AND (posts.external_url IS NULL)) OR (posts.created_by = $3) ORDER BY posts.created_at,posts.title LIMIT $4")
@@ -0,0 +1,251 @@+{-|+Module: Test.RouterSupportSpec++Tests for typed auto routing.+-}+{-# LANGUAGE AllowAmbiguousTypes #-}++module Test.RouterSupportSpec where+import ClassyPrelude+import Test.Hspec+import IHP.Test.Mocking+import IHP.Prelude+import IHP.Environment+import IHP.FrameworkConfig+import IHP.RouterSupport hiding (get)+import Data.Attoparsec.ByteString.Char8 (string, endOfInput)+import IHP.ViewPrelude+import IHP.ControllerPrelude hiding (get, request)+import Network.Wai.Test+import Network.HTTP.Types++data Band' = Band {id :: (Id' "bands"), meta :: MetaBag} deriving (Eq, Show)+type Band = Band'+type instance GetTableName (Band') = "bands"+type instance GetModelByTableName "bands" = Band+type instance GetModelName (Band') = "Band"+type instance PrimaryKey "bands" = Integer++data Performance' = Performance {id :: (Id' "performances"), meta :: MetaBag} deriving (Eq, Show)+type Performance = Performance'+type instance GetTableName (Performance') = "performances"+type instance GetModelByTableName "performances" = Performance+type instance GetModelName (Performance') = "Performance"+type instance PrimaryKey "performances" = UUID++data TextModel' = TextModel {id :: (Id' "textModel"), meta :: MetaBag} deriving (Eq, Show)+type TextModel = TextModel'+type instance GetTableName (TextModel') = "textModel"+type instance GetModelByTableName "textModel" = TextModel+type instance GetModelName (TextModel') = "textModel"+type instance PrimaryKey "textModel" = Text++data WebApplication = WebApplication deriving (Eq, Show, Data)++data TestController+ = TestAction+ | TestTextAction { firstParam :: Text }+ | TestMaybeTextAction { maybeFirstParam :: Maybe Text }+ | TestIntAction { intParam :: Int }+ | TestMaybeIntAction { maybeInt :: Maybe Int }+ | TestTextListAction { textList :: [Text] }+ | TestIntListAction { intList :: [Int] }+ | TestMixedAction { text :: Text, textOther :: Text, intList :: [Int], maybeText :: Maybe Text, textOtherOther :: Text, intParam :: Int }+ | TestInteger { p1 :: Integer, p2 :: Maybe Integer, p3 :: [Integer] }+ | TestIntegerId { integerId :: Id Band }+ | TestUUIDId { uuidId :: Id Performance }+ | TestUUIDList { uuidList :: [UUID] }+ deriving (Eq, Show, Data)++instance Controller TestController where+ action TestAction = do+ renderPlain "TestAction"+ action TestTextAction { .. } = do+ renderPlain (cs firstParam)+ action TestMaybeTextAction { .. } = do+ case maybeFirstParam of+ Just firstParam -> renderPlain ("Just " <> cs firstParam)+ Nothing -> renderPlain "Nothing"+ action TestIntAction { .. } = do+ renderPlain (cs $ ClassyPrelude.show intParam)+ action TestMaybeIntAction { .. } = do+ case maybeInt of+ Just int -> renderPlain ("Just " <> cs (ClassyPrelude.show int))+ Nothing -> renderPlain "Nothing"+ action TestTextListAction { .. } = do+ renderPlain (cs $ ClassyPrelude.show textList)+ action TestIntListAction { .. } = do+ renderPlain (cs $ ClassyPrelude.show intList)+ action TestMixedAction { .. } = do+ renderPlain (cs $+ text+ <> " " <> textOther+ <> " " <> cs (ClassyPrelude.show intList)+ <> " Nothing"+ <> " " <> textOtherOther+ <> " " <> cs (ClassyPrelude.show intParam))+ action TestInteger { .. } = do+ renderPlain (cs $+ cs (ClassyPrelude.show p1)+ <> (" " :: Text) <> cs (ClassyPrelude.show p2)+ <> " " <> cs (ClassyPrelude.show p3))+ action TestIntegerId { .. } = do+ renderPlain (cs $ ClassyPrelude.show integerId)+ action TestUUIDId { .. } = do+ renderPlain (cs $ ClassyPrelude.show uuidId)+ action TestUUIDList { .. } = do+ renderPlain $ cs $ ClassyPrelude.show uuidList++instance AutoRoute TestController where+ autoRoute = autoRouteWithIdType (parseIntegerId @(Id Band))+ applyAction = applyConstr (parseIntegerId @(Id Band))++-- | Controller for testing customRoutes/customPathTo+data CustomRouteController+ = ListPerformancesAction+ | ShowPerformanceAction { performanceId :: !(Id Performance) }+ | CreatePerformanceAction+ deriving (Eq, Show, Data)++instance Controller CustomRouteController where+ action ListPerformancesAction = renderPlain "ListPerformancesAction"+ action ShowPerformanceAction { .. } = renderPlain (cs $ ClassyPrelude.show performanceId)+ action CreatePerformanceAction = renderPlain "CreatePerformanceAction"++instance AutoRoute CustomRouteController where+ customRoutes = do+ string "/performances/"+ performanceId <- parseId+ endOfInput+ onlyAllowMethods [GET, HEAD]+ pure ShowPerformanceAction { performanceId }++ customPathTo ShowPerformanceAction { performanceId } = Just ("/performances/" <> IHP.Prelude.tshow performanceId)+ customPathTo _ = Nothing++instance FrontController WebApplication where+ controllers =+ [ parseRoute @TestController+ , parseRoute @CustomRouteController+ ]++defaultLayout :: Html -> Html+defaultLayout inner = [hsx|{inner}|]++instance InitControllerContext WebApplication where+ initContext = do+ setLayout defaultLayout++instance FrontController RootApplication where+ controllers = [ mountFrontController WebApplication ]++testGet :: ByteString -> Session SResponse+testGet url = request $ setPath defaultRequest { requestMethod = methodGet } url++assertSuccess :: ByteString -> SResponse -> IO ()+assertSuccess body response = do+ response.simpleStatus `shouldBe` status200+ response.simpleBody `shouldBe` (cs body)++assertFailure :: SResponse -> IO ()+assertFailure response = do+ response.simpleStatus `shouldBe` status400++config = do+ option Development+ option (AppPort 8000)++tests :: Spec+tests = aroundAll (withMockContextAndApp WebApplication config) do+ describe "Typed Auto Route" $ do+ it "parses empty route" $ withContextAndApp \application -> do+ runSession (testGet "test/Test") application >>= assertSuccess "TestAction"+ it "parses Text param" $ withContextAndApp \application -> do+ runSession (testGet "test/TestText?firstParam=hello") application >>= assertSuccess "hello"+ it "parses Text param with UUID value" $ withContextAndApp \application -> do+ runSession (testGet "test/TestText?firstParam=ea9cd792-107f-49ff-92a1-f610f7a31f31") application >>= assertSuccess "ea9cd792-107f-49ff-92a1-f610f7a31f31"+ it "parses Maybe Text param: Nothing" $ withContextAndApp \application -> do+ runSession (testGet "test/TestMaybeText") application >>= assertSuccess "Nothing"+ it "parses Maybe Text param: Just" $ withContextAndApp \application -> do+ runSession (testGet "test/TestMaybeText?maybeFirstParam=asdfasdf") application >>= assertSuccess "Just asdfasdf"+ it "parses Int param" $ withContextAndApp \application -> do+ runSession (testGet "test/TestInt?intParam=5432") application >>= assertSuccess "5432"+ it "parses Int param: fails on wrong type" $ withContextAndApp \application -> do+ runSession (testGet "test/TestInt?intParam=hello") application >>= assertFailure+ it "parses Maybe Int param: Nothing" $ withContextAndApp \application -> do+ runSession (testGet "test/TestMaybeInt") application >>= assertSuccess "Nothing"+ it "parses Maybe Int param: Just" $ withContextAndApp \application -> do+ runSession (testGet "test/TestMaybeInt?maybeInt=5") application >>= assertSuccess "Just 5"+ it "parses Maybe Int param: Just, wrong type" $ withContextAndApp \application -> do+ runSession (testGet "test/TestMaybeInt?maybeInt=asdf") application >>= assertSuccess "Nothing"+ it "parses [Text] param: empty" $ withContextAndApp \application -> do+ runSession (testGet "test/TestTextList") application >>= assertSuccess "[]"+ it "parses [Text] param: one element" $ withContextAndApp \application -> do+ runSession (testGet "test/TestTextList?textList=hello") application >>= assertSuccess "[\"hello\"]"+ it "parses [Text] param: multiple elements" $ withContextAndApp \application -> do+ runSession (testGet "test/TestTextList?textList=hello,sailor,beautiful,day,5") application >>= assertSuccess "[\"hello\",\"sailor\",\"beautiful\",\"day\",\"5\"]"+ it "parses [Int] param: empty" $ withContextAndApp \application -> do+ runSession (testGet "test/TestIntList") application >>= assertSuccess "[]"+ it "parses [Int] param: one element" $ withContextAndApp \application -> do+ runSession (testGet "test/TestIntList?intList=5") application >>= assertSuccess "[5]"+ it "parses [Int] param: multiple elements" $ withContextAndApp \application -> do+ runSession (testGet "test/TestIntList?intList=5,4,3") application >>= assertSuccess "[5,4,3]"+ it "parses [Int] param: ignore non-int element" $ withContextAndApp \application -> do+ runSession (testGet "test/TestIntList?intList=5,BOO,3") application >>= assertSuccess "[5,3]"+ it "parses mixed params" $ withContextAndApp \application -> do+ runSession (testGet "test/TestMixed?text=hello&textOther=sailor&intList=5,BOO,3&textOtherOther=asdf&intParam=123") application >>= assertSuccess "hello sailor [5,3] Nothing asdf 123"+ it "parses Integer params: empty" $ withContextAndApp \application -> do+ runSession (testGet "test/TestInteger?p1=1237124971624971247691279641762412786418697247869124") application >>= assertSuccess "1237124971624971247691279641762412786418697247869124 Nothing []"+ it "parses Integer params: full" $ withContextAndApp \application -> do+ runSession (testGet "test/TestInteger?p1=1237124971624971247691279641762412786418697247869124&p2=123123197269176247612461769284769812481278487124&p3=1,2,3,4") application >>= assertSuccess "1237124971624971247691279641762412786418697247869124 Just 123123197269176247612461769284769812481278487124 [1,2,3,4]"+ it "parses Id with Integer param" $ withContextAndApp \application -> do+ runSession (testGet "test/TestIntegerId?integerId=123") application >>= assertSuccess "123"+ it "parses Id with UUID param" $ withContextAndApp \application -> do+ runSession (testGet "test/TestUUIDId?uuidId=8dd57d19-490a-4323-8b94-6081ab93bf34") application >>= assertSuccess "8dd57d19-490a-4323-8b94-6081ab93bf34"+ it "parses [UUID] param: empty" $ withContextAndApp \application -> do+ runSession (testGet "test/TestUUIDList") application >>= assertSuccess "[]"+ it "parses [UUID] param: one element" $ withContextAndApp \application -> do+ runSession (testGet "test/TestUUIDList?uuidList=8dd57d19-490a-4323-8b94-6081ab93bf34") application >>= assertSuccess "[8dd57d19-490a-4323-8b94-6081ab93bf34]"+ it "parses [UUID] param: multiple elements" $ withContextAndApp \application -> do+ runSession (testGet "test/TestUUIDList?uuidList=8dd57d19-490a-4323-8b94-6081ab93bf34,8dd57d19-490a-4323-8b94-6081ab93bf34") application >>= assertSuccess "[8dd57d19-490a-4323-8b94-6081ab93bf34,8dd57d19-490a-4323-8b94-6081ab93bf34]"+ it "parses [UUID] param: multiple elements, ignoring non UUID" $ withContextAndApp \application -> do+ runSession (testGet "test/TestUUIDList?uuidList=8dd57d19-490a-4323-8b94-6081ab93bf34,423423432432432") application >>= assertSuccess "[8dd57d19-490a-4323-8b94-6081ab93bf34]"+ describe "pathTo" $ do+ it "generates correct path for empty route" $ withContextAndApp \application -> do+ pathTo TestAction `shouldBe` "/test/Test"+ it "generates correct path for Text param" $ withContextAndApp \application -> do+ pathTo (TestTextAction "hello") `shouldBe` "/test/TestText?firstParam=hello"+ it "generates correct path for Maybe Text param: Nothing" $ withContextAndApp \application -> do+ pathTo (TestMaybeTextAction Nothing) `shouldBe` "/test/TestMaybeText"+ it "generates correct path for Maybe Text param: Just" $ withContextAndApp \application -> do+ pathTo (TestMaybeTextAction (Just "hello")) `shouldBe` "/test/TestMaybeText?maybeFirstParam=hello"+ it "generates correct path for Int param" $ withContextAndApp \application -> do+ pathTo (TestIntAction 5) `shouldBe` "/test/TestInt?intParam=5"+ it "generates correct path for [Text] param: Empty" $ withContextAndApp \application -> do+ pathTo (TestTextListAction []) `shouldBe` "/test/TestTextList"+ it "generates correct path for [Text] param: Full" $ withContextAndApp \application -> do+ pathTo (TestTextListAction ["hello", "there"]) `shouldBe` "/test/TestTextList?textList=hello%2Cthere"+ it "generates correct path for [Int] param" $ withContextAndApp \application -> do+ pathTo (TestIntListAction [1,2,3]) `shouldBe` "/test/TestIntList?intList=1%2C2%2C3"+ it "generates correct path for UUID param" $ withContextAndApp \application -> do+ pathTo (TestUUIDId "8dd57d19-490a-4323-8b94-6081ab93bf34") `shouldBe` "/test/TestUUIDId?uuidId=8dd57d19-490a-4323-8b94-6081ab93bf34"+ it "generates correct path for [UUID] param" $ withContextAndApp \application -> do+ pathTo (TestUUIDList ["8dd57d19-490a-4323-8b94-6081ab93bf34", "fdb15f8e-2fe9-441a-ae0e-da56956b1722"]) `shouldBe` "/test/TestUUIDList?uuidList=8dd57d19-490a-4323-8b94-6081ab93bf34%2Cfdb15f8e-2fe9-441a-ae0e-da56956b1722"+ it "generates correct path when used with Breadcrumbs" $ withContextAndApp \application -> do+ let breadcrumb = breadcrumbLink "Test" TestAction+ breadcrumb.url `shouldBe` Just "/test/Test"+ describe "customRoutes" $ do+ it "parses custom route for overridden action" $ withContextAndApp \application -> do+ runSession (testGet "performances/8dd57d19-490a-4323-8b94-6081ab93bf34") application >>= assertSuccess "8dd57d19-490a-4323-8b94-6081ab93bf34"+ it "auto-generated route still works for overridden action" $ withContextAndApp \application -> do+ runSession (testGet "test/ShowPerformance?performanceId=8dd57d19-490a-4323-8b94-6081ab93bf34") application >>= assertSuccess "8dd57d19-490a-4323-8b94-6081ab93bf34"+ it "auto-generated route works for non-overridden actions" $ withContextAndApp \application -> do+ runSession (testGet "test/ListPerformances") application >>= assertSuccess "ListPerformancesAction"+ it "auto-generated POST route works for non-overridden actions" $ withContextAndApp \application -> do+ let postReq url = request $ setPath defaultRequest { requestMethod = methodPost } url+ runSession (postReq "test/CreatePerformance") application >>= assertSuccess "CreatePerformanceAction"+ describe "customPathTo" $ do+ it "generates custom path for overridden action" $ withContextAndApp \application -> do+ pathTo (ShowPerformanceAction "8dd57d19-490a-4323-8b94-6081ab93bf34") `shouldBe` "/performances/8dd57d19-490a-4323-8b94-6081ab93bf34"+ it "generates auto path for non-overridden action" $ withContextAndApp \application -> do+ pathTo ListPerformancesAction `shouldBe` "/test/ListPerformances"
@@ -0,0 +1,212 @@+{-|+Module: Test.ValidationSupport.ValidateFieldSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.ValidationSupport.ValidateFieldSpec where++import Test.Hspec+import IHP.Prelude+import IHP.ValidationSupport.ValidateField+import IHP.ValidationSupport.Types++tests = do+ describe "The nonEmpty validator" do+ it "should handle trivial cases" do+ nonEmpty ("hi" :: Text) `shouldBe` Success+ nonEmpty (Just "hi") `shouldBe` Success++ nonEmpty ("" :: Text) `shouldSatisfy` isFailure+ nonEmpty Nothing `shouldSatisfy` isFailure++ describe "The isEmptyValue validator" do+ it "should handle trivial cases" do+ isEmptyValue ("" :: Text) `shouldBe` Success+ isEmptyValue Nothing `shouldBe` Success++ isEmptyValue ("hi" :: Text) `shouldSatisfy` isFailure+ isEmptyValue (Just "hi") `shouldSatisfy` isFailure++ describe "The isPhoneNumber validator" do+ it "should handle trivial cases" do+ isPhoneNumber "1337" `shouldSatisfy` isFailure+ isPhoneNumber "+1337" `shouldSatisfy` isFailure+ isPhoneNumber "0049123456789" `shouldSatisfy` isFailure+ isPhoneNumber "+49123456789" `shouldBe` Success++ describe "The isEmail validator" do+ it "should handle trivial cases" do+ isEmail "foo@example.com" `shouldBe` Success+ isEmail "foobar" `shouldSatisfy` isFailure++ it "should allow subdomains" do+ isEmail "foo@bar.example.com" `shouldBe` Success++ it "should allow + chars in the user part" do+ isEmail "foo+bar@example.com" `shouldBe` Success++ it "should reject + chars in the server part" do+ isEmail "foo@bar+example.com" `shouldSatisfy` isFailure+ isEmail "foo@example.bar+com" `shouldSatisfy` isFailure++ it "should allow Unicode characters" do+ isEmail "ॐ@मणिपद्मे.हूँ" `shouldBe` Success++ it "should allow dotless domains" do+ isEmail "foo@localhost" `shouldBe` Success++ describe "The isInRange validator" do+ it "should handle trivial cases" do+ isInRange (10, 20) 15 `shouldBe` Success+ isInRange (0, 10) 0 `shouldBe` Success+ isInRange (0, 10) 10 `shouldBe` Success+ isInRange ((-10), 0) (-10) `shouldBe` Success++ isInRange (10, 20) 21 `shouldSatisfy` isFailure+ isInRange (10, 20) 9 `shouldSatisfy` isFailure++ describe "The isLessThan validator" do+ it "should handle trivial cases" do+ isLessThan 10 9 `shouldBe` Success+ isLessThan 10 (-10) `shouldBe` Success++ isLessThan 10 10 `shouldSatisfy` isFailure+ isLessThan 10 11 `shouldSatisfy` isFailure++ describe "The isGreaterThan validator" do+ it "should handle trivial cases" do+ isGreaterThan 10 11 `shouldBe` Success+ isGreaterThan (-10) 0 `shouldBe` Success++ isGreaterThan 10 10 `shouldSatisfy` isFailure+ isGreaterThan 10 9 `shouldSatisfy` isFailure++ describe "The isEqual validator" do+ it "should handle trivial cases" do+ isEqual 10 10 `shouldBe` Success+ isEqual "Foo" "Foo" `shouldBe` Success++ isEqual 10 11 `shouldSatisfy` isFailure+ isEqual "Foo" "Bar" `shouldSatisfy` isFailure++ describe "The hasMaxLength validator" do+ it "should handle trivial cases" do+ hasMaxLength 10 "foo" `shouldBe` Success+ hasMaxLength 3 "foo" `shouldBe` Success++ hasMaxLength 2 "foo" `shouldSatisfy` isFailure++ it "should handle Umlauts" do+ hasMaxLength 7 "Rösrath" `shouldBe` Success+ hasMaxLength 6 "Rösrath" `shouldSatisfy` isFailure++ {-+ it "should handle Unicode" do+ -- haha, not possible :-)++ -- What's the length of "ॐमणिपद्मेहूँ"?+ -- Okay, that was easy... what's the length of "🤦🏼♂️"?+ -}++ describe "The hasMinLength validator" do+ it "should handle trivial cases" do+ hasMinLength 3 "foo" `shouldBe` Success+ hasMinLength 1 "foo" `shouldBe` Success++ hasMinLength 4 "foo" `shouldSatisfy` isFailure++ it "should handle Umlauts" do+ hasMinLength 7 "Rösrath" `shouldBe` Success+ hasMinLength 8 "Rösrath" `shouldSatisfy` isFailure++ describe "The isRgbHexColor validator" do+ it "should handle trivial cases" do+ isRgbHexColor "#ff0000" `shouldBe` Success+ isRgbHexColor "huh?" `shouldSatisfy` isFailure++ it "should handle shorthand syntax" do+ isRgbHexColor "#fff" `shouldBe` Success++ it "should reject rgba values" do+ isRgbHexColor "#ff000000" `shouldSatisfy` isFailure+ isRgbHexColor "#f000" `shouldSatisfy` isFailure++ describe "The isRgbaHexColor validator" do+ it "should handle trivial cases" do+ isRgbaHexColor "#ff000000" `shouldBe` Success+ isRgbaHexColor "huh?" `shouldSatisfy` isFailure++ it "should handle shorthand syntax" do+ isRgbaHexColor "#ffff" `shouldBe` Success++ it "should reject rgb values" do+ isRgbaHexColor "#ff0000" `shouldSatisfy` isFailure+ isRgbaHexColor "#f00" `shouldSatisfy` isFailure++ describe "The isRgbColor validator" do+ it "should handle trivial cases" do+ isRgbColor "rgb(255, 0, 0)" `shouldBe` Success+ isRgbColor "huh?" `shouldSatisfy` isFailure++ it "should accept decimal numbers" do+ isRgbColor "rgb(255, 100.5, .4)" `shouldBe` Success++ isRgbColor "rgb(255, 100., .4)" `shouldSatisfy` isFailure+ isRgbColor "rgb(255, 100.5, .)" `shouldSatisfy` isFailure++ describe "The isRgbaColor validator" do+ it "should handle trivial cases" do+ isRgbaColor "rgba(255, 0, 0, 0)" `shouldBe` Success+ isRgbaColor "huh?" `shouldSatisfy` isFailure++ it "should accept decimal numbers" do+ isRgbaColor "rgba(255, 100.5, .4, 0)" `shouldBe` Success++ isRgbaColor "rgba(255, 100., .4, 0)" `shouldSatisfy` isFailure+ isRgbaColor "rgba(255, 100.5, ., 0)" `shouldSatisfy` isFailure++ describe "The isColor validator" do+ it "should accept hex colors" do+ isColor "#f00" `shouldBe` Success+ isColor "#ff000000" `shouldBe` Success++ it "should accept rgb(a) colors" do+ isColor "rgb(255, 0, 0)" `shouldBe` Success+ isColor "rgba(255, 0, 0, .5)" `shouldBe` Success++ describe "The isUrl validator" do+ it "should handle trivial cases" do+ isUrl "http://ihp.digitallyinduced.com/" `shouldBe` Success+ isUrl "https://ihp.digitallyinduced.com/" `shouldBe` Success++ isUrl "ihp.digitallyinduced.com" `shouldSatisfy` isFailure+ isUrl "gopher://ihp.digitallyinduced.com/" `shouldSatisfy` isFailure++ describe "The validateAny validator" do+ it "should handle trivial cases" do+ validateAny [isEmptyValue, isEmail] "" `shouldBe` Success+ validateAny [isGreaterThan(10), isLessThan(5)] 1 `shouldBe` Success+ validateAny [isGreaterThan(10), isLessThan(5)] 11 `shouldBe` Success+ validateAny [isGreaterThan(10), isLessThan(5)] 7 `shouldSatisfy` isFailure++ describe "The validateAll validator" do+ it "should handle trivial cases" do+ validateAll [isGreaterThan(1900), isLessThan(2020)] 2016 `shouldBe` Success+ validateAll [isGreaterThan(1900), isLessThan(2020)] 2050 `shouldSatisfy` isFailure++ describe "The isInList validator" do+ it "should handle trivial cases" do+ isInList [1954, 1974, 1990, 2014] 2014 `shouldBe` Success+ isInList [1954, 1974, 1990, 2014] 2018 `shouldSatisfy` isFailure++ isInList ["C", "Haskell", "Rust"] "Haskell" `shouldBe` Success+ isInList ["C", "Haskell", "Rust"] "JavaScript" `shouldSatisfy` isFailure -- rightly!++ describe "The isTrue validator" do+ it "should handle trivial cases" do+ isTrue True `shouldBe` Success+ isTrue False `shouldSatisfy` isFailure++ describe "The isFalse validator" do+ it "should handle trivial cases" do+ isFalse False `shouldBe` Success+ isFalse True `shouldSatisfy` isFailure
@@ -0,0 +1,729 @@+{-|+Module: Test.View.CSSFrameworkSpec+Copyright: (c) digitally induced GmbH, 2020+-}+module Test.View.CSSFrameworkSpec where++import Test.Hspec+import IHP.Prelude+import IHP.Controller.Context+import IHP.FrameworkConfig as FrameworkConfig+import Wai.Request.Params.Middleware (RequestBody (..))+import IHP.View.Types+import IHP.View.CSSFramework+import Network.Wai.Middleware.FlashMessages (FlashMessage (..))+import qualified Text.Blaze.Renderer.Text as Blaze+import IHP.ModelSupport+import IHP.Breadcrumb.ViewFunctions (breadcrumbLinkExternal, breadcrumbText, renderBreadcrumb)+import IHP.Pagination.Types+import qualified IHP.Prelude as Text (isInfixOf)+import qualified Data.TMap as TypeMap+import qualified Network.Wai as Wai+import IHP.Pagination.ViewFunctions (renderPagination)+import qualified Data.Vault.Lazy as Vault+import qualified IHP.RequestVault++tests = do+ describe "CSS Framework" do+ describe "bootstrap5" do+ let cssFramework = bootstrap++ it "should render a successful flash message" do+ let flashMessage = SuccessFlashMessage "You have successfully registered for an account"+ styledFlashMessage cssFramework cssFramework flashMessage `shouldRenderTo` "<div class=\"alert alert-success\">You have successfully registered for an account</div>"++ it "should render a error flash message" do+ let flashMessage = ErrorFlashMessage "You have successfully registered for an account"+ styledFlashMessage cssFramework cssFramework flashMessage `shouldRenderTo` "<div class=\"alert alert-danger\">You have successfully registered for an account</div>"++ describe "submit button" do+ let submitButton = SubmitButton { label = "Save Project" , buttonClass = "my-custom-button" , cssFramework, buttonDisabled = False }+ it "should render" do+ styledSubmitButton cssFramework cssFramework submitButton `shouldRenderTo` "<button class=\"btn btn-primary my-custom-button\" type=\"submit\">Save Project</button>"++ it "should render with empty class" do+ styledSubmitButton cssFramework cssFramework (submitButton { buttonClass = "" }) `shouldRenderTo` "<button class=\"btn btn-primary\" type=\"submit\">Save Project</button>"++ it "should render with disabled button" do+ styledSubmitButton cssFramework cssFramework (submitButton { buttonClass = "", buttonDisabled = True }) `shouldRenderTo` "<button class=\"btn btn-primary\" disabled=\"disabled\" type=\"submit\">Save Project</button>"++ describe "text field" do+ let baseTextField = FormField+ { fieldType = TextInput+ , fieldName = "firstname"+ , fieldLabel = "First name:"+ , fieldValue = ""+ , fieldInputId = "fname"+ , validatorResult = Nothing+ , additionalAttributes = []+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = "Your firstname"+ , required = False+ , autofocus = False+ }+ it "should render" do+ let textField = baseTextField+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-fname\"><label class=\"form-label\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> </div>"++ it "should render with disabled" do+ let textField = baseTextField { disabled = True }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-fname\"><label class=\"form-label\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\" disabled=\"disabled\"> </div>"++ it "should render a validation error" do+ let textField = baseTextField { validatorResult = Just (TextViolation "should not be empty") }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-fname\"><label class=\"form-label\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control is-invalid\"><div class=\"invalid-feedback\">should not be empty</div> </div>"++ it "should render with disableLabel = True" do+ let textField = baseTextField { disableLabel = True }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-fname\"><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> </div>"++ it "should render with disableGroup = True" do+ let textField = baseTextField { disableGroup = True }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<label class=\"form-label\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> "++ it "should render with help text" do+ let textField = baseTextField { helpText = "Enter your first name" }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-fname\"><label class=\"form-label\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> <small class=\"form-text\">Enter your first name</small></div>"++ it "should render with additional attributes" do+ let textField = baseTextField { additionalAttributes = [ ("min", "1"), ("max", "100") ] }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-fname\"><label class=\"form-label\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\" min=\"1\" max=\"100\"> </div>"++ describe "hidden field" do+ let baseTextField = FormField+ { fieldType = HiddenInput+ , fieldName = "workerProfileId"+ , fieldLabel = "Worker Profile Id"+ , fieldValue = ""+ , fieldInputId = "award_workerProfileId"+ , validatorResult = Nothing+ , additionalAttributes = []+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = True+ , disableGroup = True+ , disableValidationResult = True+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = ""+ , required = False+ , autofocus = False+ }+ it "should render without a group and without validation" do+ let textField = baseTextField+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<input type=\"hidden\" name=\"workerProfileId\" placeholder=\"\" id=\"award_workerProfileId\" class=\"form-control\"> "+++ describe "checkbox" do+ let baseCheckbox = FormField+ { fieldType = CheckboxInput+ , fieldName = "is_active"+ , fieldLabel = "Is Active"+ , fieldValue = "yes"+ , fieldInputId = "user_is_active"+ , validatorResult = Nothing+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = ""+ , required = False+ , autofocus = False+ }+ it "should render" do+ let checkbox = baseCheckbox+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ it "should render with disabled" do+ let checkbox = baseCheckbox { disabled = True }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\" disabled=\"disabled\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ it "should render without checked" do+ let checkbox = baseCheckbox { fieldValue = "no" }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ it "should render with disableLabel = True" do+ let checkbox = baseCheckbox { disableLabel = True }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-user_is_active\"><div class=\"form-check\"><div><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"> </div></div></div>"++ it "should render with disableGroup = True" do+ let checkbox = baseCheckbox { disableGroup = True }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div>"++ it "should render with help text" do+ let checkbox = baseCheckbox { helpText = "Is the user active?" }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> <small class=\"form-text\">Is the user active?</small></div></div>"++ it "should render with additional attributes" do+ let checkbox = baseCheckbox { additionalAttributes = [ ("data-x", "true") ] }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\" data-x=\"true\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ describe "select" do+ let baseSelect = FormField+ { fieldType = SelectInput [("First Value", "a"), ("Second Value", "b")]+ , fieldName = "user_id"+ , fieldLabel = "User"+ , fieldValue = ""+ , fieldInputId = "project_user_id"+ , validatorResult = Nothing+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = "Please select"+ , required = False+ , autofocus = False+ }++ it "should render" do+ let select = baseSelect+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ it "should render with disabled" do+ let select = baseSelect { disabled = True }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\" disabled=\"disabled\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ it "should render with selected" do+ let select = baseSelect { fieldValue = "b" }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"b\"><option value=\"\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\" selected=\"selected\">Second Value</option></select> </div>"++ it "should render with custom placeholder" do+ let select = baseSelect { placeholder = "Pick something" }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Pick something</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ it "should render with additional attributes" do+ let select = baseSelect { additionalAttributes = [ ("data-x", "true") ] }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\" data-x=\"true\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ describe "radio" do+ let baseRadio = FormField+ { fieldType = RadioInput [("First Value", "a"), ("Second Value", "b")]+ , fieldName = "user_id"+ , fieldLabel = "User"+ , fieldValue = ""+ , fieldInputId = "project_user_id"+ , validatorResult = Nothing+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = ""+ , required = False+ , autofocus = False+ }++ it "should render" do+ let radio = baseRadio+ styledFormField cssFramework cssFramework radio `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><fieldset><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_a\" name=\"user_id\" value=\"a\"><label class=\"form-check-label\" for=\"project_user_id_a\">First Value</label></div><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_b\" name=\"user_id\" value=\"b\"><label class=\"form-check-label\" for=\"project_user_id_b\">Second Value</label></div></fieldset> </div>"++ it "should render with disabled" do+ let radio = baseRadio { disabled = True }+ styledFormField cssFramework cssFramework radio `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><fieldset><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_a\" name=\"user_id\" value=\"a\" disabled=\"disabled\"><label class=\"form-check-label\" for=\"project_user_id_a\">First Value</label></div><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_b\" name=\"user_id\" value=\"b\" disabled=\"disabled\"><label class=\"form-check-label\" for=\"project_user_id_b\">Second Value</label></div></fieldset> </div>"++ it "should render with checked" do+ let radio = baseRadio { fieldValue = "b" }+ styledFormField cssFramework cssFramework radio `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><fieldset><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_a\" name=\"user_id\" value=\"a\"><label class=\"form-check-label\" for=\"project_user_id_a\">First Value</label></div><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_b\" name=\"user_id\" value=\"b\" checked=\"checked\"><label class=\"form-check-label\" for=\"project_user_id_b\">Second Value</label></div></fieldset> </div>"++ it "should render with additional attributes" do+ let radio = baseRadio { additionalAttributes = [ ("data-x", "true") ] }+ styledFormField cssFramework cssFramework radio `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-project_user_id\"><label class=\"form-label\" for=\"project_user_id\">User</label><fieldset><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_a\" name=\"user_id\" value=\"a\" data-x=\"true\"><label class=\"form-check-label\" for=\"project_user_id_a\">First Value</label></div><div class=\"form-check\"><input class=\"form-check-input\" type=\"radio\" id=\"project_user_id_b\" name=\"user_id\" value=\"b\" data-x=\"true\"><label class=\"form-check-label\" for=\"project_user_id_b\">Second Value</label></div></fieldset> </div>"++ describe "textarea field" do+ let baseTextField = FormField+ { fieldType = TextareaInput+ , fieldName = "body"+ , fieldLabel = "Body:"+ , fieldValue = "Hello\nWorld!"+ , fieldInputId = "body"+ , validatorResult = Nothing+ , additionalAttributes = []+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = "Describe your issue"+ , required = False+ , autofocus = False+ }+ it "should render" do+ let textField = baseTextField+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-body\"><label class=\"form-label\" for=\"body\">Body:</label><textarea name=\"body\" placeholder=\"Describe your issue\" id=\"body\" class=\"form-control\">Hello\nWorld!</textarea></div>"++ it "should render with additional attributes" do+ let textField = baseTextField { additionalAttributes = [ ("data-x", "true") ] }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"mb-3\" id=\"form-group-body\"><label class=\"form-label\" for=\"body\">Body:</label><textarea name=\"body\" placeholder=\"Describe your issue\" id=\"body\" class=\"form-control\" data-x=\"true\">Hello\nWorld!</textarea></div>"++ describe "pagination" do+ let basePagination = Pagination+ {+ pageSize = 3+ , totalItems = 12+ , currentPage = 2+ , window = 3+ }+ it "should render previous link" do+ let pagination = basePagination+ styledPaginationLinkPrevious cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">«</span><span class=\"visually-hidden\">Previous</span></a></li>"++ it "should render previous link disabled on the first page" do+ let pagination = basePagination { currentPage = 1}+ styledPaginationLinkPrevious cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item disabled\"><a class=\"page-link\" href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">«</span><span class=\"visually-hidden\">Previous</span></a></li>"++ it "should render next link" do+ let pagination = basePagination+ styledPaginationLinkNext cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">»</span><span class=\"visually-hidden\">Next</span></a></li>"++ it "should render next link disabled on the last page" do+ let pagination = basePagination { currentPage = 4}+ styledPaginationLinkNext cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item disabled\"><a class=\"page-link\" href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">»</span><span class=\"visually-hidden\">Next</span></a></li>"++ it "should render items per page selector" do+ let pagination = basePagination+ styledPaginationItemsPerPageSelector cssFramework cssFramework pagination (\n -> cs $ "https://example.com?maxItems=" <> (show n)) `shouldRenderTo` "<option value=\"10\" data-url=\"https://example.com?maxItems=10\">10 items per page</option><option value=\"20\" data-url=\"https://example.com?maxItems=20\">20 items per page</option><option value=\"50\" data-url=\"https://example.com?maxItems=50\">50 items per page</option><option value=\"100\" data-url=\"https://example.com?maxItems=100\">100 items per page</option><option value=\"200\" data-url=\"https://example.com?maxItems=200\">200 items per page</option>"++ it "should render the wrapping pagination" do+ let pagination = basePagination+ let paginationView = PaginationView+ { cssFramework = cssFramework+ , pagination = pagination+ , pageUrl = const ""+ , linkPrevious = mempty+ , linkNext = mempty+ , pageDotDotItems = mempty+ , itemsPerPageSelector = mempty+ }++ let render = Blaze.renderMarkup $ styledPagination cssFramework cssFramework paginationView+ Text.isInfixOf "<div class=\"d-flex justify-content-md-center\">" (cs render) `shouldBe` True++ it "should render the pagination if there are enough elements" do+ let pagination = Pagination+ { pageSize = 5+ , totalItems = 15+ , currentPage = 1+ , window = 3+ }++ context <- createControllerContextWithCSSFramework cssFramework+ let ?context = context+ let ?request = ?context.request++ let render = Blaze.renderMarkup $ renderPagination pagination+ Text.isInfixOf "<nav aria-label=\"Page Navigator\"" (cs render) `shouldBe` True+++ it "should not render the pagination if there aren't enough elements" do+ let pagination = Pagination+ { pageSize = 10+ , totalItems = 5+ , currentPage = 1+ , window = 3+ }++ context <- createControllerContextWithCSSFramework cssFramework+ let ?context = context+ let ?request = ?context.request++ renderPagination pagination `shouldRenderTo` mempty+++ describe "breadcrumbs" do+ it "should render a breadcrumb item with no link" do+ let breadcrumbItem = breadcrumbText "First item"+ let breadcrumbs = [breadcrumbItem]++ styledBreadcrumbItem cssFramework cssFramework breadcrumbs breadcrumbItem True `shouldRenderTo` "<li class=\"breadcrumb-item active\">First item</li>"++ it "should render a breadcrumb item with external link" do+ let breadcrumbItem = breadcrumbLinkExternal "First item" "https://example.com"+ let breadcrumbs = [breadcrumbItem]++ styledBreadcrumbItem cssFramework cssFramework breadcrumbs breadcrumbItem True `shouldRenderTo` "<li class=\"breadcrumb-item active\"><a href=\"https://example.com\">First item</a></li>"++ it "should render a breadcrumb item as non-active" do+ let breadcrumbItem = breadcrumbText "First item"+ let breadcrumbs = [breadcrumbItem]++ styledBreadcrumbItem cssFramework cssFramework breadcrumbs breadcrumbItem False `shouldRenderTo` "<li class=\"breadcrumb-item\">First item</li>"++ it "should render the wrapping breadcrumb and last item as active" do+ let breadcrumbs = [breadcrumbText "First item", breadcrumbText "Last item"]++ context <- createControllerContextWithCSSFramework cssFramework+ let ?context = context+ let ?request = ?context.request++ renderBreadcrumb breadcrumbs `shouldRenderTo` "<nav><ol class=\"breadcrumb\"><li class=\"breadcrumb-item\">First item</li><li class=\"breadcrumb-item active\">Last item</li></ol></nav>"++ it "should support show of BreadcrumbItem" do+ let breadcrumbItem = breadcrumbText "First item"+ show breadcrumbItem `shouldBe` "{ breadcrumbLabel = \"First item\", url = Nothing }"++ describe "bootstrap4" do+ let cssFramework = bootstrap4++ it "should render a successful flash message" do+ let flashMessage = SuccessFlashMessage "You have successfully registered for an account"+ styledFlashMessage cssFramework cssFramework flashMessage `shouldRenderTo` "<div class=\"alert alert-success\">You have successfully registered for an account</div>"++ it "should render a error flash message" do+ let flashMessage = ErrorFlashMessage "You have successfully registered for an account"+ styledFlashMessage cssFramework cssFramework flashMessage `shouldRenderTo` "<div class=\"alert alert-danger\">You have successfully registered for an account</div>"++ describe "submit button" do+ let submitButton = SubmitButton { label = "Save Project" , buttonClass = "my-custom-button" , cssFramework, buttonDisabled = False }+ it "should render" do+ styledSubmitButton cssFramework cssFramework submitButton `shouldRenderTo` "<button class=\"btn btn-primary my-custom-button\" type=\"submit\">Save Project</button>"++ it "should render with empty class" do+ styledSubmitButton cssFramework cssFramework (submitButton { buttonClass = "" }) `shouldRenderTo` "<button class=\"btn btn-primary\" type=\"submit\">Save Project</button>"++ it "should render with disabled button" do+ styledSubmitButton cssFramework cssFramework (submitButton { buttonClass = "", buttonDisabled = True }) `shouldRenderTo` "<button class=\"btn btn-primary\" disabled=\"disabled\" type=\"submit\">Save Project</button>"+++ describe "text field" do+ let baseTextField = FormField+ { fieldType = TextInput+ , fieldName = "firstname"+ , fieldLabel = "First name:"+ , fieldValue = ""+ , fieldInputId = "fname"+ , validatorResult = Nothing+ , additionalAttributes = []+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = "Your firstname"+ , required = False+ , autofocus = False+ }+ it "should render" do+ let textField = baseTextField+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-fname\"><label class=\"\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> </div>"++ it "should render with disabled" do+ let textField = baseTextField { disabled = True }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-fname\"><label class=\"\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\" disabled=\"disabled\"> </div>"++ it "should render a validation error" do+ let textField = baseTextField { validatorResult = Just (TextViolation "should not be empty") }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-fname\"><label class=\"\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control is-invalid\"><div class=\"invalid-feedback\">should not be empty</div> </div>"++ it "should render with disableLabel = True" do+ let textField = baseTextField { disableLabel = True }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-fname\"><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> </div>"++ it "should render with disableGroup = True" do+ let textField = baseTextField { disableGroup = True }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<label class=\"\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> "++ it "should render with help text" do+ let textField = baseTextField { helpText = "Enter your first name" }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-fname\"><label class=\"\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\"> <small class=\"form-text text-muted\">Enter your first name</small></div>"++ it "should render with additional attributes" do+ let textField = baseTextField { additionalAttributes = [ ("min", "1"), ("max", "100") ] }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-fname\"><label class=\"\" for=\"fname\">First name:</label><input type=\"text\" name=\"firstname\" placeholder=\"Your firstname\" id=\"fname\" class=\"form-control\" min=\"1\" max=\"100\"> </div>"+++ describe "checkbox" do+ let baseCheckbox = FormField+ { fieldType = CheckboxInput+ , fieldName = "is_active"+ , fieldLabel = "Is Active"+ , fieldValue = "yes"+ , fieldInputId = "user_is_active"+ , validatorResult = Nothing+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = ""+ , required = False+ , autofocus = False+ }+ it "should render" do+ let checkbox = baseCheckbox+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ it "should render with disabled" do+ let checkbox = baseCheckbox { disabled = True }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\" disabled=\"disabled\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ it "should render without checked" do+ let checkbox = baseCheckbox { fieldValue = "no" }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ it "should render with disableLabel = True" do+ let checkbox = baseCheckbox { disableLabel = True }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-user_is_active\"><div class=\"form-check\"><div><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"> </div></div></div>"++ it "should render with disableGroup = True" do+ let checkbox = baseCheckbox { disableGroup = True }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div>"++ it "should render with help text" do+ let checkbox = baseCheckbox { helpText = "Is the user active?" }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> <small class=\"form-text text-muted\">Is the user active?</small></div></div>"++ it "should render with additional attributes" do+ let checkbox = baseCheckbox { additionalAttributes = [ ("data-x", "true") ] }+ styledFormField cssFramework cssFramework checkbox `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-user_is_active\"><div class=\"form-check\"><input type=\"checkbox\" name=\"is_active\" class=\"form-check-input\" id=\"user_is_active\" checked=\"checked\" data-x=\"true\"><input type=\"hidden\" name=\"is_active\" value=\"off\"><label class=\"form-check-label\" for=\"user_is_active\">Is Active</label> </div></div>"++ describe "select" do+ let baseSelect = FormField+ { fieldType = SelectInput [("First Value", "a"), ("Second Value", "b")]+ , fieldName = "user_id"+ , fieldLabel = "User"+ , fieldValue = ""+ , fieldInputId = "project_user_id"+ , validatorResult = Nothing+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , additionalAttributes = []+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = "Please select"+ , required = False+ , autofocus = False+ }++ it "should render" do+ let select = baseSelect+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-project_user_id\"><label class=\"\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ it "should render with disabled" do+ let select = baseSelect { disabled = True }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-project_user_id\"><label class=\"\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\" disabled=\"disabled\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ it "should render with selected" do+ let select = baseSelect { fieldValue = "b" }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-project_user_id\"><label class=\"\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"b\"><option value=\"\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\" selected=\"selected\">Second Value</option></select> </div>"++ it "should render with custom placeholder" do+ let select = baseSelect { placeholder = "Pick something" }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-project_user_id\"><label class=\"\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Pick something</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ it "should render with additional attributes" do+ let select = baseSelect { additionalAttributes = [ ("data-x", "true") ] }+ styledFormField cssFramework cssFramework select `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-project_user_id\"><label class=\"\" for=\"project_user_id\">User</label><select name=\"user_id\" id=\"project_user_id\" class=\"form-control\" value=\"\" data-x=\"true\"><option value=\"\" selected=\"selected\" disabled=\"disabled\">Please select</option> <option value=\"a\">First Value</option><option value=\"b\">Second Value</option></select> </div>"++ describe "textarea field" do+ let baseTextField = FormField+ { fieldType = TextareaInput+ , fieldName = "body"+ , fieldLabel = "Body:"+ , fieldValue = "Hello\nWorld!"+ , fieldInputId = "body"+ , validatorResult = Nothing+ , additionalAttributes = []+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = "Describe your issue"+ , required = False+ , autofocus = False+ }+ it "should render" do+ let textField = baseTextField+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-body\"><label class=\"\" for=\"body\">Body:</label><textarea name=\"body\" placeholder=\"Describe your issue\" id=\"body\" class=\"form-control\">Hello\nWorld!</textarea></div>"++ it "should render with additional attributes" do+ let textField = baseTextField { additionalAttributes = [ ("data-x", "true") ] }+ styledFormField cssFramework cssFramework textField `shouldRenderTo` "<div class=\"form-group\" id=\"form-group-body\"><label class=\"\" for=\"body\">Body:</label><textarea name=\"body\" placeholder=\"Describe your issue\" id=\"body\" class=\"form-control\" data-x=\"true\">Hello\nWorld!</textarea></div>"++ describe "pagination" do+ let basePagination = Pagination+ {+ pageSize = 3+ , totalItems = 12+ , currentPage = 2+ , window = 3+ }+ it "should render previous link" do+ let pagination = basePagination+ styledPaginationLinkPrevious cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">«</span><span class=\"sr-only\">Previous</span></a></li>"++ it "should render previous link disabled on the first page" do+ let pagination = basePagination { currentPage = 1}+ styledPaginationLinkPrevious cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item disabled\"><a class=\"page-link\" href=\"#\" aria-label=\"Previous\"><span aria-hidden=\"true\">«</span><span class=\"sr-only\">Previous</span></a></li>"++ it "should render next link" do+ let pagination = basePagination+ styledPaginationLinkNext cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item\"><a class=\"page-link\" href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">»</span><span class=\"sr-only\">Next</span></a></li>"++ it "should render next link disabled on the last page" do+ let pagination = basePagination { currentPage = 4}+ styledPaginationLinkNext cssFramework cssFramework pagination "#" `shouldRenderTo` "<li class=\"page-item disabled\"><a class=\"page-link\" href=\"#\" aria-label=\"Next\"><span aria-hidden=\"true\">»</span><span class=\"sr-only\">Next</span></a></li>"++ it "should render items per page selector" do+ let pagination = basePagination+ styledPaginationItemsPerPageSelector cssFramework cssFramework pagination (\n -> cs $ "https://example.com?maxItems=" <> (show n)) `shouldRenderTo` "<option value=\"10\" data-url=\"https://example.com?maxItems=10\">10 items per page</option><option value=\"20\" data-url=\"https://example.com?maxItems=20\">20 items per page</option><option value=\"50\" data-url=\"https://example.com?maxItems=50\">50 items per page</option><option value=\"100\" data-url=\"https://example.com?maxItems=100\">100 items per page</option><option value=\"200\" data-url=\"https://example.com?maxItems=200\">200 items per page</option>"++ it "should render the wrapping pagination" do+ let pagination = basePagination+ let paginationView = PaginationView+ { cssFramework = cssFramework+ , pagination = pagination+ , pageUrl = const ""+ , linkPrevious = mempty+ , linkNext = mempty+ , pageDotDotItems = mempty+ , itemsPerPageSelector = mempty+ }++ let render = Blaze.renderMarkup $ styledPagination cssFramework cssFramework paginationView+ Text.isInfixOf "<div class=\"d-flex justify-content-md-center\">" (cs render) `shouldBe` True++ it "should render the pagination if there are enough elements" do+ let pagination = Pagination+ { pageSize = 5+ , totalItems = 15+ , currentPage = 1+ , window = 3+ }++ context <- createControllerContextWithCSSFramework cssFramework+ let ?context = context+ let ?request = ?context.request++ let render = Blaze.renderMarkup $ renderPagination pagination+ Text.isInfixOf "<nav aria-label=\"Page Navigator\"" (cs render) `shouldBe` True+++ it "should not render the pagination if there aren't enough elements" do+ let pagination = Pagination+ { pageSize = 10+ , totalItems = 5+ , currentPage = 1+ , window = 3+ }++ context <- createControllerContextWithCSSFramework cssFramework+ let ?context = context+ let ?request = ?context.request++ renderPagination pagination `shouldRenderTo` mempty+++ describe "breadcrumbs" do+ it "should render a breadcrumb item with no link" do+ let breadcrumbItem = breadcrumbText "First item"+ let breadcrumbs = [breadcrumbItem]++ styledBreadcrumbItem cssFramework cssFramework breadcrumbs breadcrumbItem True `shouldRenderTo` "<li class=\"breadcrumb-item active\">First item</li>"++ it "should render a breadcrumb item with external link" do+ let breadcrumbItem = breadcrumbLinkExternal "First item" "https://example.com"+ let breadcrumbs = [breadcrumbItem]++ styledBreadcrumbItem cssFramework cssFramework breadcrumbs breadcrumbItem True `shouldRenderTo` "<li class=\"breadcrumb-item active\"><a href=\"https://example.com\">First item</a></li>"++ it "should render a breadcrumb item as non-active" do+ let breadcrumbItem = breadcrumbText "First item"+ let breadcrumbs = [breadcrumbItem]++ styledBreadcrumbItem cssFramework cssFramework breadcrumbs breadcrumbItem False `shouldRenderTo` "<li class=\"breadcrumb-item\">First item</li>"++ it "should render the wrapping breadcrumb and last item as active" do+ let breadcrumbs = [breadcrumbText "First item", breadcrumbText "Last item"]++ context <- createControllerContextWithCSSFramework cssFramework+ let ?context = context+ let ?request = ?context.request++ renderBreadcrumb breadcrumbs `shouldRenderTo` "<nav><ol class=\"breadcrumb\"><li class=\"breadcrumb-item\">First item</li><li class=\"breadcrumb-item active\">Last item</li></ol></nav>"++ it "should support show of BreadcrumbItem" do+ let breadcrumbItem = breadcrumbText "First item"+ show breadcrumbItem `shouldBe` "{ breadcrumbLabel = \"First item\", url = Nothing }"++ describe "tailwind" do+ let cssFramework = tailwind++ describe "textareaField" do+ let baseTextField = FormField+ { fieldType = TextareaInput+ , fieldName = "body"+ , fieldLabel = "Body:"+ , fieldValue = "Hello\nWorld!"+ , fieldInputId = "body"+ , validatorResult = Nothing+ , additionalAttributes = []+ , fieldClass = ""+ , labelClass = ""+ , disabled = False+ , disableLabel = False+ , disableGroup = False+ , disableValidationResult = False+ , cssFramework = cssFramework+ , helpText = ""+ , placeholder = "Describe your issue"+ , required = False+ , autofocus = False+ }+ it "should render tailwind label classes" do+ styledFormField cssFramework cssFramework baseTextField `shouldRenderTo` "<div class=\"flex flex-col my-6 space-y-2\" id=\"form-group-body\"><label class=\"font-medium text-gray-700\" for=\"body\">Body:</label><textarea name=\"body\" placeholder=\"Describe your issue\" id=\"body\" class=\"focus:ring-blue-500 focus:border-blue-500 block w-full border-gray-300 rounded-md\">Hello\nWorld!</textarea></div>"++++shouldRenderTo renderFunction expectedHtml = Blaze.renderMarkup renderFunction `shouldBe` expectedHtml++{-| Mock a Controller context with CSSFramework.+-}+createControllerContextWithCSSFramework :: Typeable option => option -> IO ControllerContext+createControllerContextWithCSSFramework cssFramework = do+ frameworkConfig <- FrameworkConfig.buildFrameworkConfig do+ option cssFramework+ let requestBody = FormBody { params = [], files = [], rawPayload = "" }+ let request = Wai.defaultRequest { Wai.vault = Vault.insert IHP.RequestVault.frameworkConfigVaultKey frameworkConfig+ $ Vault.insert IHP.RequestVault.requestBodyVaultKey requestBody Vault.empty }+ let customFields = TypeMap.insert request TypeMap.empty+ pure FrozenControllerContext { customFields }
@@ -0,0 +1,194 @@+{-|+Module: Test.View.FormSpec+Copyright: (c) digitally induced GmbH, 2022+-}+module Test.View.FormSpec where++import Test.Hspec+import IHP.FrameworkConfig as FrameworkConfig+import Wai.Request.Params.Middleware (RequestBody (..))+import qualified Text.Blaze.Renderer.Text as Blaze+import IHP.ModelSupport+import Data.Bits ((.|.))+import qualified Network.Wai as Wai+import IHP.ViewPrelude+import qualified Data.Text.Lazy as LT+import qualified Data.Vault.Lazy as Vault+import qualified IHP.RequestVault+import qualified Data.TMap as TypeMap+++tests = do+ describe "IHP.Form" do+ describe "formFor" do+ let project = newRecord @Project++ it "should render a form" do+ context <- createControllerContext+ let ?context = context+ let ?request = ?context.request++ let form = formFor project [hsx|+ {textField #title}+ {submitButton}+ |]+ form `shouldRenderTo` "<form method=\"POST\" action=\"/CreateProject\" id=\"\" class=\"new-form\" data-disable-javascript-submission=\"false\"><div class=\"mb-3\" id=\"form-group-project_title\"><label class=\"form-label\" for=\"project_title\">Title</label><input type=\"text\" name=\"title\" placeholder=\"\" id=\"project_title\" class=\"form-control\"> </div> <button class=\"btn btn-primary\" type=\"submit\">Create Project</button></form>"++ it "should render a form with a GET method" do+ context <- createControllerContext+ let ?context = context+ let ?request = ?context.request++ let options formContext = formContext |> set #formMethod "GET"++ let form = formForWithOptions project options [hsx|+ {textField #title}+ {submitButton}+ |]+ form `shouldRenderTo` "<form method=\"GET\" action=\"/CreateProject\" id=\"\" class=\"new-form\" data-disable-javascript-submission=\"false\"><div class=\"mb-3\" id=\"form-group-project_title\"><label class=\"form-label\" for=\"project_title\">Title</label><input type=\"text\" name=\"title\" placeholder=\"\" id=\"project_title\" class=\"form-control\"> </div> <button class=\"btn btn-primary\" type=\"submit\">Create Project</button></form>"++ it "should render a date field with empty value attribute when value is Nothing" do+ context <- createControllerContext+ let ?context = context+ let ?request = ?context.request+ let event = newRecord @Event++ let form = formFor event [hsx|+ {(dateField #date) { required = True }}+ |]+ + -- The date input should have value="" (not omit the value attribute)+ -- This is necessary for HTML5 required validation to work properly+ let rendered = Blaze.renderMarkup form+ let renderedText = LT.toStrict rendered+ renderedText `shouldSatisfy` (\t -> "type=\"date\"" `isInfixOf` t)+ renderedText `shouldSatisfy` (\t -> "required=\"required\"" `isInfixOf` t)+ renderedText `shouldSatisfy` (\t -> "value=\"\"" `isInfixOf` t)++ it "should render a datetime field with empty value attribute when value is Nothing" do+ context <- createControllerContext+ let ?context = context+ let ?request = ?context.request+ let event = newRecord @Event++ let form = formFor event [hsx|+ {(dateTimeField #createdAt) { required = True }}+ |]+ + -- The datetime input should have value="" (not omit the value attribute)+ let rendered = Blaze.renderMarkup form+ let renderedText = LT.toStrict rendered+ renderedText `shouldSatisfy` (\t -> "type=\"datetime-local\"" `isInfixOf` t)+ renderedText `shouldSatisfy` (\t -> "required=\"required\"" `isInfixOf` t)+ renderedText `shouldSatisfy` (\t -> "value=\"\"" `isInfixOf` t)++shouldRenderTo renderFunction expectedHtml = Blaze.renderMarkup renderFunction `shouldBe` expectedHtml++createControllerContext :: IO ControllerContext+createControllerContext = do+ frameworkConfig <- FrameworkConfig.buildFrameworkConfig (pure ())+ let requestBody = FormBody { params = [], files = [], rawPayload = "" }+ let request = Wai.defaultRequest { Wai.vault = Vault.insert IHP.RequestVault.frameworkConfigVaultKey frameworkConfig+ $ Vault.insert IHP.RequestVault.requestBodyVaultKey requestBody Vault.empty }+ let customFields = TypeMap.insert request TypeMap.empty+ pure FrozenControllerContext { customFields }++data Project' = Project {id :: (Id' "projects"), title :: Text, meta :: MetaBag} deriving (Eq, Show)+instance InputValue Project where inputValue = IHP.ModelSupport.recordToInputValue+type Project = Project'++type instance GetTableName (Project' ) = "projects"+type instance GetModelByTableName "projects" = Project+type instance GetModelName (Project' ) = "Project"++type instance PrimaryKey "projects" = UUID++instance Record Project where+ {-# INLINE newRecord #-}+ newRecord = Project def def def+instance Default (Id' "projects") where def = Id def++instance SetField "id" (Project' ) (Id' "projects") where+ {-# INLINE setField #-}+ setField newValue (Project id title meta) =+ Project newValue title (meta { touchedFields = touchedFields meta .|. 1 })+instance SetField "title" (Project' ) Text where+ {-# INLINE setField #-}+ setField newValue (Project id title meta) =+ Project id newValue (meta { touchedFields = touchedFields meta .|. 2 })+instance SetField "meta" (Project' ) MetaBag where+ {-# INLINE setField #-}+ setField newValue (Project id title meta) =+ Project id title newValue+instance UpdateField "id" (Project' ) (Project' ) (Id' "projects") (Id' "projects") where+ {-# INLINE updateField #-}+ updateField newValue (Project id title meta) = Project newValue title (meta { touchedFields = touchedFields meta .|. 1 })+instance UpdateField "title" (Project' ) (Project' ) Text Text where+ {-# INLINE updateField #-}+ updateField newValue (Project id title meta) = Project id newValue (meta { touchedFields = touchedFields meta .|. 2 })+instance UpdateField "meta" (Project' ) (Project' ) MetaBag MetaBag where+ {-# INLINE updateField #-}+ updateField newValue (Project id title meta) = Project id title newValue+instance FieldBit "id" (Project' ) where fieldBit = 1+instance FieldBit "title" (Project' ) where fieldBit = 2++-- Event model for testing date fields+data Event' = Event + { id :: (Id' "events")+ , date :: Maybe Day+ , createdAt :: Maybe UTCTime+ , meta :: MetaBag+ } deriving (Eq, Show)++instance InputValue Event where inputValue = IHP.ModelSupport.recordToInputValue+type Event = Event'++type instance GetTableName (Event' ) = "events"+type instance GetModelByTableName "events" = Event+type instance GetModelName (Event' ) = "Event"++type instance PrimaryKey "events" = UUID++instance Record Event where+ {-# INLINE newRecord #-}+ newRecord = Event def def def def+instance Default (Id' "events") where def = Id def++instance SetField "id" (Event' ) (Id' "events") where+ {-# INLINE setField #-}+ setField newValue (Event id date createdAt meta) =+ Event newValue date createdAt (meta { touchedFields = touchedFields meta .|. 1 })+instance SetField "date" (Event' ) (Maybe Day) where+ {-# INLINE setField #-}+ setField newValue (Event id date createdAt meta) =+ Event id newValue createdAt (meta { touchedFields = touchedFields meta .|. 2 })+instance SetField "createdAt" (Event' ) (Maybe UTCTime) where+ {-# INLINE setField #-}+ setField newValue (Event id date createdAt meta) =+ Event id date newValue (meta { touchedFields = touchedFields meta .|. 4 })+instance SetField "meta" (Event' ) MetaBag where+ {-# INLINE setField #-}+ setField newValue (Event id date createdAt meta) =+ Event id date createdAt newValue++instance UpdateField "id" (Event' ) (Event' ) (Id' "events") (Id' "events") where+ {-# INLINE updateField #-}+ updateField newValue (Event id date createdAt meta) = Event newValue date createdAt (meta { touchedFields = touchedFields meta .|. 1 })+instance UpdateField "date" (Event' ) (Event' ) (Maybe Day) (Maybe Day) where+ {-# INLINE updateField #-}+ updateField newValue (Event id date createdAt meta) = Event id newValue createdAt (meta { touchedFields = touchedFields meta .|. 2 })+instance UpdateField "createdAt" (Event' ) (Event' ) (Maybe UTCTime) (Maybe UTCTime) where+ {-# INLINE updateField #-}+ updateField newValue (Event id date createdAt meta) = Event id date newValue (meta { touchedFields = touchedFields meta .|. 4 })+instance UpdateField "meta" (Event' ) (Event' ) MetaBag MetaBag where+ {-# INLINE updateField #-}+ updateField newValue (Event id date createdAt meta) = Event id date createdAt newValue+instance FieldBit "id" (Event' ) where fieldBit = 1+instance FieldBit "date" (Event' ) where fieldBit = 2+instance FieldBit "createdAt" (Event' ) where fieldBit = 4++++++
@@ -0,0 +1,111 @@+{-|+Module: Test.ViewSupportSpec++Tests for view support functions.+-}+{-# LANGUAGE AllowAmbiguousTypes #-}++module Test.ViewSupportSpec where+import ClassyPrelude+import Test.Hspec+import IHP.Test.Mocking+import IHP.Prelude+import IHP.Environment+import IHP.FrameworkConfig+import IHP.RouterSupport hiding (get)+import IHP.ViewPrelude+import IHP.ControllerPrelude hiding (get, request)+import Network.Wai.Test+import Network.HTTP.Types+import Data.Text as Text++data WebApplication = WebApplication deriving (Eq, Show, Data)++data TestController+ = TestAction+ | TestWithParamAction { param :: Text }+ deriving (Eq, Show, Data)++data AnotherTestController+ = AnotherTestAction+ deriving (Eq, Show, Data)++instance Controller TestController where+ action TestAction = do+ renderPlain "TestAction"+ action TestWithParamAction { .. } = do+ render ShowView { .. }++instance Controller AnotherTestController where+ action AnotherTestAction = do+ renderPlain "AnotherTestAction"++instance AutoRoute TestController+instance AutoRoute AnotherTestController++instance FrontController WebApplication where+ controllers = [ parseRoute @TestController, parseRoute @AnotherTestController ]++data ShowView = ShowView { param :: Text}++instance View ShowView where+ html ShowView { .. }= [hsx|+ isActiveAction {param}: {isActiveAction $ TestWithParamAction param}+ isActiveAction bar: {isActiveAction $ TestWithParamAction "bar"}++ isActivePath {param}: {isActivePath $ "/test/TestWithParam?param=" <> param}+ isActivePath bar: {isActivePath ("/test/TestWithParam?param=bar" :: Text)}++ isActiveController TestController: {isActiveController @TestController}+ isActiveController AnotherTestAction: {isActiveController @AnotherTestController}+ |]++defaultLayout :: Html -> Html+defaultLayout inner = [hsx|{inner}|]++instance InitControllerContext WebApplication where+ initContext = do+ setLayout defaultLayout++instance FrontController RootApplication where+ controllers = [ mountFrontController WebApplication ]++testGet :: ByteString -> Session SResponse+testGet url = request $ setPath defaultRequest { requestMethod = methodGet } url++assertTextExists :: Text -> SResponse -> IO ()+assertTextExists body response = do+ response.simpleStatus `shouldBe` status200+ Text.isInfixOf body (cs response.simpleBody) `shouldBe` True++config = do+ option Development+ option (AppPort 8000)++tests :: Spec+tests = aroundAll (withMockContextAndApp WebApplication config) do+ describe "isActiveAction" $ do+ it "should return True on the same route" $ withContextAndApp \application -> do+ runSession (testGet "test/TestWithParam?param=foo") application >>= assertTextExists "isActiveAction foo: True"+ it "should return False on a different route" $ withContextAndApp \application -> do+ runSession (testGet "test/TestWithParam?param=foo") application >>= assertTextExists "isActiveAction bar: False"+ describe "isActivePath" $ do+ it "should return True on the same route" $ withContextAndApp \application -> do+ runSession (testGet "test/TestWithParam?param=foo") application >>= assertTextExists "isActivePath foo: True"+ it "should return False on a different route" $ withContextAndApp \application -> do+ runSession (testGet "test/TestWithParam?param=foo") application >>= assertTextExists "isActivePath bar: False"+ describe "isActiveController" $ do+ it "should return True on the same route" $ withContextAndApp \application -> do+ runSession (testGet "test/TestWithParam?param=foo") application >>= assertTextExists "isActiveController TestController: True"+ it "should return False on a different route" $ withContextAndApp \application -> do+ runSession (testGet "test/TestWithParam?param=foo") application >>= assertTextExists "isActiveController AnotherTestAction: False"++ describe "HSX" $ do+ it "allow using Id's in HSX attributes without explicitly calling inputValue" $ withContextAndApp \_ -> do+ let+ id :: Id' "users"+ id = Id ("70a10b53-a776-470a-91a8-900cdda06aa2" :: UUID)++ (ClassyPrelude.tshow [hsx|<input value={id} />|]) `shouldBe` "<input value=\"70a10b53-a776-470a-91a8-900cdda06aa2\">"++type instance PrimaryKey "users" = UUID
@@ -169,8 +169,8 @@ // We cannot use `form.action` here because there could be a <input name="action"/> // See https://github.com/digitallyinduced/ihp/issues/1203 var formAction = (possibleClickedButton && possibleClickedButton.getAttribute('formAction'))- ? possibleClickedButton.getAttribute('formAction')- : form.getAttribute('action');+ ? possibleClickedButton.getAttribute('formAction')+ : form.getAttribute('action'); var formMethod = form.getAttribute('method') || 'GET'; var request = new XMLHttpRequest();@@ -389,15 +389,26 @@ if (!('flatpickr' in window)) { return; }- flatpickr("input[type='date']", {- altFormat: 'd.m.y',++ document.querySelectorAll("input[type='date']").forEach(el => {+ flatpickr(el, {+ ...(el.dataset.altFormat ? {} : { altFormat: 'd.m.y' }),+ ...(el.dataset.altInput ? {} : { altInput: true }),+ }); });- flatpickr("input[type='datetime-local']", {- enableTime: true,- time_24hr: true,- dateFormat: 'Z',- altInput: true,- altFormat: 'd.m.y, H:i',++ document.querySelectorAll("input[type='datetime-local']").forEach(el => {+ // Append Z so Flatpickr knows the server value is UTC+ if (el.value && !el.value.endsWith('Z')) {+ el.value = el.value + 'Z';+ }+ flatpickr(el, {+ ...(el.dataset.enableTime ? {} : { enableTime: true }),+ ...(el.dataset.time_24hr ? {} : { time_24hr: true }),+ ...(el.dataset.dateFormat ? {} : { dateFormat: 'Z' }),+ ...(el.dataset.altFormat ? {} : { altFormat: 'd.m.y, H:i' }),+ ...(el.dataset.altInput ? {} : { altInput: true }),+ }); }); }
@@ -1,7 +0,0 @@-@charset "UTF-8";/*!- * Bootstrap v5.2.1 (https://getbootstrap.com/)- * Copyright 2011-2022 The Bootstrap Authors- * Copyright 2011-2022 Twitter, Inc.- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)- */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-2xl:2rem;--bs-border-radius-pill:50rem;--bs-link-color:#0d6efd;--bs-link-hover-color:#0a58ca;--bs-code-color:#d63384;--bs-highlight-bg:#fff3cd}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:1px solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:var(--bs-link-color);text-decoration:underline}a:hover{color:var(--bs-link-hover-color)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid var(--bs-border-color);border-radius:.375rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color:var(--bs-body-color);--bs-table-bg:transparent;--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-body-color);--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:var(--bs-body-color);--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:var(--bs-body-color);--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:var(--bs-table-color);vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:2px solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#bacbe6;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#cbccce;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#bcd0c7;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#badce3;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#e6dbb9;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#dfc2c4;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#dfe0e1;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#373b3e;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:calc(1.5em + .75rem + 2px);padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:.375rem}.form-control-color::-webkit-color-swatch{border-radius:.375rem}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + 2px)}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + 2px)}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.375rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.25rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.5rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;width:100%;height:100%;padding:1rem .75rem;overflow:hidden;text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::-moz-placeholder,.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:not(:-moz-placeholder-shown),.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control-plaintext~label{border-width:1px 0}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.375rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.25rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.375rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.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(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.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-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.375rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%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='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%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='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:#212529;--bs-btn-bg:transparent;--bs-btn-border-width:1px;--bs-btn-border-color:transparent;--bs-btn-border-radius:0.375rem;--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:first-child:hover,:not(.btn-check)+.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:none;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:0.5rem}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:0.25rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:#212529;--bs-dropdown-bg:#fff;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:0.375rem;--bs-dropdown-border-width:1px;--bs-dropdown-inner-border-radius:calc(0.375rem - 1px);--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-dropdown-link-color:#212529;--bs-dropdown-link-hover-color:#1e2125;--bs-dropdown-link-hover-bg:#e9ecef;--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:.375rem}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:#6c757d;display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link.disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:1px;--bs-nav-tabs-border-color:#dee2e6;--bs-nav-tabs-border-radius:0.375rem;--bs-nav-tabs-link-hover-border-color:#e9ecef #e9ecef #dee2e6;--bs-nav-tabs-link-active-color:#495057;--bs-nav-tabs-link-active-bg:#fff;--bs-nav-tabs-link-active-border-color:#dee2e6 #dee2e6 #fff;border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));background:0 0;border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-link.disabled,.nav-tabs .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:0.375rem;--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{background:0 0;border:0;border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link:disabled{color:var(--bs-nav-link-disabled-color);background-color:transparent;border-color:transparent}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(0, 0, 0, 0.55);--bs-navbar-hover-color:rgba(0, 0, 0, 0.7);--bs-navbar-disabled-color:rgba(0, 0, 0, 0.3);--bs-navbar-active-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(0, 0, 0, 0.9);--bs-navbar-brand-hover-color:rgba(0, 0, 0, 0.9);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(0, 0, 0, 0.1);--bs-navbar-toggler-border-radius:0.375rem;--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .show>.nav-link{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-border-width:1px;--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:0.375rem;--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(0.375rem - 1px);--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(0, 0, 0, 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:#fff;--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:#fff;--bs-accordion-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,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:1px;--bs-accordion-border-radius:0.375rem;--bs-accordion-inner-border-radius:calc(0.375rem - 1px);--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='var%28--bs-body-color%29'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");--bs-accordion-btn-focus-border-color:#86b7fe;--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:#0c63e4;--bs-accordion-active-bg:#e7f1ff}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:var(--bs-accordion-btn-focus-border-color);outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button,.accordion-flush .accordion-item .accordion-button.collapsed{border-radius:0}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:#6c757d;--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:#6c757d;display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:#fff;--bs-pagination-border-width:1px;--bs-pagination-border-color:#dee2e6;--bs-pagination-border-radius:0.375rem;--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:#e9ecef;--bs-pagination-hover-border-color:#dee2e6;--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:#e9ecef;--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:#6c757d;--bs-pagination-disabled-bg:#fff;--bs-pagination-disabled-border-color:#dee2e6;display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:0.5rem}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:0.25rem}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:0.375rem;display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:1px solid var(--bs-alert-border-color);--bs-alert-border-radius:0.375rem;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:#084298;--bs-alert-bg:#cfe2ff;--bs-alert-border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{--bs-alert-color:#41464b;--bs-alert-bg:#e2e3e5;--bs-alert-border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{--bs-alert-color:#0f5132;--bs-alert-bg:#d1e7dd;--bs-alert-border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{--bs-alert-color:#055160;--bs-alert-bg:#cff4fc;--bs-alert-border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{--bs-alert-color:#664d03;--bs-alert-bg:#fff3cd;--bs-alert-border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{--bs-alert-color:#842029;--bs-alert-bg:#f8d7da;--bs-alert-border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{--bs-alert-color:#636464;--bs-alert-bg:#fefefe;--bs-alert-border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{--bs-alert-color:#141619;--bs-alert-bg:#d3d3d4;--bs-alert-border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:#e9ecef;--bs-progress-border-radius:0.375rem;--bs-progress-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{--bs-list-group-color:#212529;--bs-list-group-bg:#fff;--bs-list-group-border-color:rgba(0, 0, 0, 0.125);--bs-list-group-border-width:1px;--bs-list-group-border-radius:0.375rem;--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:#495057;--bs-list-group-action-hover-color:#495057;--bs-list-group-action-hover-bg:#f8f9fa;--bs-list-group-action-active-color:#212529;--bs-list-group-action-active-bg:#e9ecef;--bs-list-group-disabled-color:#6c757d;--bs-list-group-disabled-bg:#fff;--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.375rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(255, 255, 255, 0.85);--bs-toast-border-width:1px;--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:0.375rem;--bs-toast-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-toast-header-color:#6c757d;--bs-toast-header-bg:rgba(255, 255, 255, 0.85);--bs-toast-header-border-color:rgba(0, 0, 0, 0.05);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color: ;--bs-modal-bg:#fff;--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:1px;--bs-modal-border-radius:0.5rem;--bs-modal-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-modal-inner-border-radius:calc(0.5rem - 1px);--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:1px;--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:1px;position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin:calc(-.5 * var(--bs-modal-header-padding-y)) calc(-.5 * var(--bs-modal-header-padding-x)) calc(-.5 * var(--bs-modal-header-padding-y)) auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:#fff;--bs-tooltip-bg:#000;--bs-tooltip-border-radius:0.375rem;--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;padding:var(--bs-tooltip-arrow-height);margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);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;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:#fff;--bs-popover-border-width:1px;--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:0.5rem;--bs-popover-inner-border-radius:calc(0.5rem - 1px);--bs-popover-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color: ;--bs-popover-header-bg:#f0f0f0;--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:#212529;--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);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;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;-webkit-animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name);animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color: ;--bs-offcanvas-bg:#fff;--bs-offcanvas-border-width:1px;--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075)}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:575.98px){.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}}@media (max-width:575.98px){.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:767.98px){.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:767.98px){.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}}@media (max-width:767.98px){.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:991.98px){.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}}@media (max-width:991.98px){.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1199.98px){.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}}@media (max-width:1199.98px){.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}}@media (max-width:1399.98px){.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}}@media (max-width:1399.98px){.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y))}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(13,110,253,var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(108,117,125,var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(25,135,84,var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(13,202,240,var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(255,193,7,var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(220,53,69,var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(248,249,250,var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(33,37,41,var(--bs-bg-opacity,1))!important}.link-primary{color:#0d6efd!important}.link-primary:focus,.link-primary:hover{color:#0a58ca!important}.link-secondary{color:#6c757d!important}.link-secondary:focus,.link-secondary:hover{color:#565e64!important}.link-success{color:#198754!important}.link-success:focus,.link-success:hover{color:#146c43!important}.link-info{color:#0dcaf0!important}.link-info:focus,.link-info:hover{color:#3dd5f3!important}.link-warning{color:#ffc107!important}.link-warning:focus,.link-warning:hover{color:#ffcd39!important}.link-danger{color:#dc3545!important}.link-danger:focus,.link-danger:hover{color:#b02a37!important}.link-light{color:#f8f9fa!important}.link-light:focus,.link-light:hover{color:#f9fafb!important}.link-dark{color:#212529!important}.link-dark:focus,.link-dark:hover{color:#1a1e21!important}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-1{--bs-border-width:1px}.border-2{--bs-border-width:2px}.border-3{--bs-border-width:3px}.border-4{--bs-border-width:4px}.border-5{--bs-border-width:5px}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-semibold{font-weight:600!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-2xl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}-//# sourceMappingURL=bootstrap.min.css.map
@@ -1,7 +0,0 @@-/*!- * Bootstrap v5.2.1 (https://getbootstrap.com/)- * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)- */-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){if(t&&t.__esModule)return t;const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s="transitionend",n=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},o=t=>{const e=n(t);return e&&document.querySelector(e)?e:null},r=t=>{const e=n(t);return e?document.querySelector(e):null},a=t=>{t.dispatchEvent(new Event(s))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,p=[],m=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of p)t()})),p.push(e)):e()},v=t=>{"function"==typeof t&&t()},y=(t,e,i=!0)=>{if(!i)return void v(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let o=!1;const r=({target:i})=>{i===e&&(o=!0,e.removeEventListener(s,r),v(t))};e.addEventListener(s,r),setTimeout((()=>{o||a(e)}),n)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,T=/\..*/,E=/::\d+$/,C={};let k=1;const L={mouseenter:"mouseover",mouseleave:"mouseout"},I=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function O(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function S(t){const e=O(t);return t.uidEvent=e,C[e]=C[e]||{},C[e]}function D(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function N(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=j(t);return I.has(o)||(o=t),[s,n,o]}function P(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=N(e,i,s);if(e in L){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=S(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=O(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&$.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&$.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function x(t,e,i,s,n){const o=D(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function M(t,e,i,s){const n=e[i]||{};for(const o of Object.keys(n))if(o.includes(s)){const s=n[o];x(t,e,i,s.callable,s.delegationSelector)}}function j(t){return t=t.replace(T,""),L[t]||t}const $={on(t,e,i,s){P(t,e,i,s,!1)},one(t,e,i,s){P(t,e,i,s,!0)},off(t,e,i,s){if("string"!=typeof e||!t)return;const[n,o,r]=N(e,i,s),a=r!==e,l=S(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))M(t,l,i,e.slice(1));for(const i of Object.keys(c)){const s=i.replace(E,"");if(!a||e.includes(s)){const e=c[i];x(t,l,r,e.callable,e.delegationSelector)}}}else{if(!Object.keys(c).length)return;x(t,l,r,o,n?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const s=f();let n=null,o=!0,r=!0,a=!1;e!==j(e)&&s&&(n=s.Event(e,i),s(t).trigger(n),o=!n.isPropagationStopped(),r=!n.isImmediatePropagationStopped(),a=n.isDefaultPrevented());let l=new Event(e,{bubbles:o,cancelable:!0});return l=F(l,i),a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&n&&n.preventDefault(),l}};function F(t,e){for(const[i,s]of Object.entries(e||{}))try{t[i]=s}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>s})}return t}const z=new Map,H={set(t,e,i){z.has(t)||z.set(t,new Map);const s=z.get(t);s.has(e)||0===s.size?s.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(t,e)=>z.has(t)&&z.get(t).get(e)||null,remove(t,e){if(!z.has(t))return;const i=z.get(t);i.delete(e),0===i.size&&z.delete(t)}};function q(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function B(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const W={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${B(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${B(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const s of i){let i=s.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=q(t.dataset[s])}return e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${B(e)}`))};class R{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=l(e)?W.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...l(e)?W.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const s of Object.keys(e)){const n=e[s],o=t[s],r=l(o)?"element":null==(i=o)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(n).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${s}" provided type "${r}" but expected type "${n}".`)}var i}}class V extends R{constructor(t,e){super(),(t=c(t))&&(this._element=t,this._config=this._getConfig(e),H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),$.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){y(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return H.get(c(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.2.1"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const K=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;$.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),d(this))return;const n=r(this)||this.closest(`.${s}`);t.getOrCreateInstance(n)[e]()}))};class Q extends V{static get NAME(){return"alert"}close(){if($.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),$.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}K(Q,"close"),b(Q);const X='[data-bs-toggle="button"]';class Y extends V{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}$.on(document,"click.bs.button.data-api",X,(t=>{t.preventDefault();const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),b(Y);const U={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let s=t.parentNode.closest(e);for(;s;)i.push(s),s=s.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!d(t)&&h(t)))}},G={endCallback:null,leftCallback:null,rightCallback:null},J={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class Z extends R{constructor(t,e){super(),this._element=t,t&&Z.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return G}static get DefaultType(){return J}static get NAME(){return"swipe"}dispose(){$.off(this._element,".bs.swipe")}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),v(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&v(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?($.on(this._element,"pointerdown.bs.swipe",(t=>this._start(t))),$.on(this._element,"pointerup.bs.swipe",(t=>this._end(t))),this._element.classList.add("pointer-event")):($.on(this._element,"touchstart.bs.swipe",(t=>this._start(t))),$.on(this._element,"touchmove.bs.swipe",(t=>this._move(t))),$.on(this._element,"touchend.bs.swipe",(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const tt="next",et="prev",it="left",st="right",nt="slid.bs.carousel",ot="carousel",rt="active",at={ArrowLeft:st,ArrowRight:it},lt={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ct={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ht extends V{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=U.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===ot&&this.cycle()}static get Default(){return lt}static get DefaultType(){return ct}static get NAME(){return"carousel"}next(){this._slide(tt)}nextWhenVisible(){!document.hidden&&h(this._element)&&this.next()}prev(){this._slide(et)}pause(){this._isSliding&&a(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?$.one(this._element,nt,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void $.one(this._element,nt,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const s=t>i?tt:et;this._slide(s,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&$.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&($.on(this._element,"mouseenter.bs.carousel",(()=>this.pause())),$.on(this._element,"mouseleave.bs.carousel",(()=>this._maybeEnableCycle()))),this._config.touch&&Z.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of U.find(".carousel-item img",this._element))$.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(it)),rightCallback:()=>this._slide(this._directionToOrder(st)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new Z(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=at[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=U.findOne(".active",this._indicatorsElement);e.classList.remove(rt),e.removeAttribute("aria-current");const i=U.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(rt),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),s=t===tt,n=e||w(this._getItems(),i,s,this._config.wrap);if(n===i)return;const o=this._getItemIndex(n),r=e=>$.trigger(this._element,e,{relatedTarget:n,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r("slide.bs.carousel").defaultPrevented)return;if(!i||!n)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=n;const l=s?"carousel-item-start":"carousel-item-end",c=s?"carousel-item-next":"carousel-item-prev";n.classList.add(c),g(n),i.classList.add(l),n.classList.add(l),this._queueCallback((()=>{n.classList.remove(l,c),n.classList.add(rt),i.classList.remove(rt,c,l),this._isSliding=!1,r(nt)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return U.findOne(".active.carousel-item",this._element)}_getItems(){return U.find(".carousel-item",this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return m()?t===it?et:tt:t===it?tt:et}_orderToDirection(t){return m()?t===et?it:st:t===et?st:it}static jQueryInterface(t){return this.each((function(){const e=ht.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}$.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",(function(t){const e=r(this);if(!e||!e.classList.contains(ot))return;t.preventDefault();const i=ht.getOrCreateInstance(e),s=this.getAttribute("data-bs-slide-to");return s?(i.to(s),void i._maybeEnableCycle()):"next"===W.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),$.on(window,"load.bs.carousel.data-api",(()=>{const t=U.find('[data-bs-ride="carousel"]');for(const e of t)ht.getOrCreateInstance(e)})),b(ht);const dt="show",ut="collapse",_t="collapsing",gt='[data-bs-toggle="collapse"]',ft={parent:null,toggle:!0},pt={parent:"(null|element)",toggle:"boolean"};class mt extends V{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=U.find(gt);for(const t of i){const e=o(t),i=U.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return ft}static get DefaultType(){return pt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>mt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if($.trigger(this._element,"show.bs.collapse").defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(ut),this._element.classList.add(_t),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_t),this._element.classList.add(ut,dt),this._element.style[e]="",$.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if($.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,g(this._element),this._element.classList.add(_t),this._element.classList.remove(ut,dt);for(const t of this._triggerArray){const e=r(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_t),this._element.classList.add(ut),$.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(dt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=c(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(gt);for(const e of t){const t=r(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=U.find(":scope .collapse .collapse",this._config.parent);return U.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=mt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}$.on(document,"click.bs.collapse.data-api",gt,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=o(this),i=U.find(e);for(const t of i)mt.getOrCreateInstance(t,{toggle:!1}).toggle()})),b(mt);const bt="dropdown",vt="ArrowUp",yt="ArrowDown",wt="click.bs.dropdown.data-api",At="keydown.bs.dropdown.data-api",Tt="show",Et='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',Ct=`${Et}.show`,kt=".dropdown-menu",Lt=m()?"top-end":"top-start",It=m()?"top-start":"top-end",Ot=m()?"bottom-end":"bottom-start",St=m()?"bottom-start":"bottom-end",Dt=m()?"left-start":"right-start",Nt=m()?"right-start":"left-start",Pt={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},xt={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Mt extends V{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=U.next(this._element,kt)[0]||U.prev(this._element,kt)[0],this._inNavbar=this._detectNavbar()}static get Default(){return Pt}static get DefaultType(){return xt}static get NAME(){return bt}toggle(){return this._isShown()?this.hide():this.show()}show(){if(d(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!$.trigger(this._element,"show.bs.dropdown",t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))$.on(t,"mouseover",_);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Tt),this._element.classList.add(Tt),$.trigger(this._element,"shown.bs.dropdown",t)}}hide(){if(d(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!$.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))$.off(t,"mouseover",_);this._popper&&this._popper.destroy(),this._menu.classList.remove(Tt),this._element.classList.remove(Tt),this._element.setAttribute("aria-expanded","false"),W.removeDataAttribute(this._menu,"popper"),$.trigger(this._element,"hidden.bs.dropdown",t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!l(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${bt.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===i)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:l(this._config.reference)?t=c(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const e=this._getPopperConfig();this._popper=i.createPopper(t,this._menu,e)}_isShown(){return this._menu.classList.contains(Tt)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Dt;if(t.classList.contains("dropstart"))return Nt;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?It:Lt:e?St:Ot}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(W.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=U.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>h(t)));i.length&&w(i,e,t===yt,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Mt.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=U.find(Ct);for(const i of e){const e=Mt.getInstance(i);if(!e||!1===e._config.autoClose)continue;const s=t.composedPath(),n=s.includes(e._menu);if(s.includes(e._element)||"inside"===e._config.autoClose&&!n||"outside"===e._config.autoClose&&n)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,s=[vt,yt].includes(t.key);if(!s&&!i)return;if(e&&!i)return;t.preventDefault();const n=this.matches(Et)?this:U.prev(this,Et)[0]||U.next(this,Et)[0],o=Mt.getOrCreateInstance(n);if(s)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),n.focus())}}$.on(document,At,Et,Mt.dataApiKeydownHandler),$.on(document,At,kt,Mt.dataApiKeydownHandler),$.on(document,wt,Mt.clearMenus),$.on(document,"keyup.bs.dropdown.data-api",Mt.clearMenus),$.on(document,wt,Et,(function(t){t.preventDefault(),Mt.getOrCreateInstance(this).toggle()})),b(Mt);const jt=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",$t=".sticky-top",Ft="padding-right",zt="margin-right";class Ht{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,Ft,(e=>e+t)),this._setElementAttributes(jt,Ft,(e=>e+t)),this._setElementAttributes($t,zt,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,Ft),this._resetElementAttributes(jt,Ft),this._resetElementAttributes($t,zt)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const s=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+s)return;this._saveInitialAttribute(t,e);const n=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(n))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&W.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=W.getDataAttribute(t,e);null!==i?(W.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(l(t))e(t);else for(const i of U.find(t,this._element))e(i)}}const qt="show",Bt="mousedown.bs.backdrop",Wt={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Rt={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Vt extends R{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Wt}static get DefaultType(){return Rt}static get NAME(){return"backdrop"}show(t){if(!this._config.isVisible)return void v(t);this._append();const e=this._getElement();this._config.isAnimated&&g(e),e.classList.add(qt),this._emulateAnimation((()=>{v(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(qt),this._emulateAnimation((()=>{this.dispose(),v(t)}))):v(t)}dispose(){this._isAppended&&($.off(this._element,Bt),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=c(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),$.on(t,Bt,(()=>{v(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){y(t,this._getElement(),this._config.isAnimated)}}const Kt=".bs.focustrap",Qt="backward",Xt={autofocus:!0,trapElement:null},Yt={autofocus:"boolean",trapElement:"element"};class Ut extends R{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return Xt}static get DefaultType(){return Yt}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),$.off(document,Kt),$.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),$.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,$.off(document,Kt))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=U.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===Qt?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Qt:"forward")}}const Gt="hidden.bs.modal",Jt="show.bs.modal",Zt="modal-open",te="show",ee="modal-static",ie={backdrop:!0,focus:!0,keyboard:!0},se={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class ne extends V{constructor(t,e){super(t,e),this._dialog=U.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new Ht,this._addEventListeners()}static get Default(){return ie}static get DefaultType(){return se}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||$.trigger(this._element,Jt,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Zt),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&($.trigger(this._element,"hide.bs.modal").defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(te),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){for(const t of[window,this._dialog])$.off(t,".bs.modal");this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Vt({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ut({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=U.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),g(this._element),this._element.classList.add(te),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,$.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){$.on(this._element,"keydown.dismiss.bs.modal",(t=>{if("Escape"===t.key)return this._config.keyboard?(t.preventDefault(),void this.hide()):void this._triggerBackdropTransition()})),$.on(window,"resize.bs.modal",(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),$.on(this._element,"mousedown.dismiss.bs.modal",(t=>{$.one(this._element,"click.dismiss.bs.modal",(e=>{this._dialog.contains(t.target)||this._dialog.contains(e.target)||("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Zt),this._resetAdjustments(),this._scrollBar.reset(),$.trigger(this._element,Gt)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if($.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(ee)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(ee),this._queueCallback((()=>{this._element.classList.remove(ee),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=m()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=m()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=ne.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}$.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=r(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),$.one(e,Jt,(t=>{t.defaultPrevented||$.one(e,Gt,(()=>{h(this)&&this.focus()}))}));const i=U.findOne(".modal.show");i&&ne.getInstance(i).hide(),ne.getOrCreateInstance(e).toggle(this)})),K(ne),b(ne);const oe="show",re="showing",ae="hiding",le=".offcanvas.show",ce="hidePrevented.bs.offcanvas",he="hidden.bs.offcanvas",de={backdrop:!0,keyboard:!0,scroll:!1},ue={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class _e extends V{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return de}static get DefaultType(){return ue}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||$.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new Ht).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(re),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(oe),this._element.classList.remove(re),$.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&($.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(ae),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(oe,ae),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new Ht).reset(),$.trigger(this._element,he)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Vt({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():$.trigger(this._element,ce)}:null})}_initializeFocusTrap(){return new Ut({trapElement:this._element})}_addEventListeners(){$.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():$.trigger(this._element,ce))}))}static jQueryInterface(t){return this.each((function(){const e=_e.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}$.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=r(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this))return;$.one(e,he,(()=>{h(this)&&this.focus()}));const i=U.findOne(le);i&&i!==e&&_e.getInstance(i).hide(),_e.getOrCreateInstance(e).toggle(this)})),$.on(window,"load.bs.offcanvas.data-api",(()=>{for(const t of U.find(le))_e.getOrCreateInstance(t).show()})),$.on(window,"resize.bs.offcanvas",(()=>{for(const t of U.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&_e.getOrCreateInstance(t).hide()})),K(_e),b(_e);const ge=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),fe=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,pe=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,me=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!ge.has(i)||Boolean(fe.test(t.nodeValue)||pe.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},be={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},ve={allowList:be,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"<div></div>"},ye={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},we={entry:"(string|element|function|null)",selector:"(string|element)"};class Ae extends R{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return ve}static get DefaultType(){return ye}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},we)}_setContent(t,e,i){const s=U.findOne(i,t);s&&((e=this._resolvePossibleFunction(e))?l(e)?this._putElementInTemplate(c(e),s):this._config.html?s.innerHTML=this._maybeSanitize(e):s.textContent=e:s.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const s=(new window.DOMParser).parseFromString(t,"text/html"),n=[].concat(...s.body.querySelectorAll("*"));for(const t of n){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const s=[].concat(...t.attributes),n=[].concat(e["*"]||[],e[i]||[]);for(const e of s)me(e,n)||t.removeAttribute(e.nodeName)}return s.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return"function"==typeof t?t(this):t}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const Te=new Set(["sanitize","allowList","sanitizeFn"]),Ee="fade",Ce="show",ke=".modal",Le="hide.bs.modal",Ie="hover",Oe="focus",Se={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},De={allowList:be,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,0],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',title:"",trigger:"hover focus"},Ne={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class Pe extends V{constructor(t,e){if(void 0===i)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners()}static get Default(){return De}static get DefaultType(){return Ne}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled){if(t){const e=this._initializeOnDelegatedTarget(t);return e._activeTrigger.click=!e._activeTrigger.click,void(e._isWithActiveTrigger()?e._enter():e._leave())}this._isShown()?this._leave():this._enter()}}dispose(){clearTimeout(this._timeout),$.off(this._element.closest(ke),Le,this._hideModalHandler),this.tip&&this.tip.remove(),this._config.originalTitle&&this._element.setAttribute("title",this._config.originalTitle),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=$.trigger(this._element,this.constructor.eventName("show")),e=(u(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this.tip&&(this.tip.remove(),this.tip=null);const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:s}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(s.append(i),$.trigger(this._element,this.constructor.eventName("inserted"))),this._popper?this._popper.update():this._popper=this._createPopper(i),i.classList.add(Ce),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))$.on(t,"mouseover",_);this._queueCallback((()=>{$.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(!this._isShown())return;if($.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented)return;const t=this._getTipElement();if(t.classList.remove(Ce),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))$.off(t,"mouseover",_);this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||t.remove(),this._element.removeAttribute("aria-describedby"),$.trigger(this._element,this.constructor.eventName("hidden")),this._disposePopper())}),this.tip,this._isAnimated())}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(Ee,Ce),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(Ee),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Ae({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._config.originalTitle}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(Ee)}_isShown(){return this.tip&&this.tip.classList.contains(Ce)}_createPopper(t){const e="function"==typeof this._config.placement?this._config.placement.call(this,t,this._element):this._config.placement,s=Se[e.toUpperCase()];return i.createPopper(this._element,t,this._getPopperConfig(s))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)$.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>this.toggle(t)));else if("manual"!==e){const t=e===Ie?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===Ie?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");$.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?Oe:Ie]=!0,e._enter()})),$.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?Oe:Ie]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},$.on(this._element.closest(ke),Le,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._config.originalTitle;t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=W.getDataAttributes(this._element);for(const t of Object.keys(e))Te.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:c(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.originalTitle=this._element.getAttribute("title")||"","number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=Pe.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(Pe);const xe={...Pe.Default,content:"",offset:[0,8],placement:"right",template:'<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>',trigger:"click"},Me={...Pe.DefaultType,content:"(null|string|element|function)"};class je extends Pe{static get Default(){return xe}static get DefaultType(){return Me}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=je.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}b(je);const $e="click.bs.scrollspy",Fe="active",ze="[href]",He={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},qe={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Be extends V{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return He}static get DefaultType(){return qe}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=c(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&($.off(this._config.target,$e),$.on(this._config.target,$e,ze,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,s=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:s,behavior:"smooth"});i.scrollTop=s}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},s=(this._rootElement||document.documentElement).scrollTop,n=s>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=s;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(n&&t){if(i(o),!s)return}else n||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=U.find(ze,this._config.target);for(const e of t){if(!e.hash||d(e))continue;const t=U.findOne(e.hash,this._element);h(t)&&(this._targetLinks.set(e.hash,e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Fe),this._activateParents(t),$.trigger(this._element,"activate.bs.scrollspy",{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))U.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Fe);else for(const e of U.parents(t,".nav, .list-group"))for(const t of U.prev(e,".nav-link, .nav-item > .nav-link, .list-group-item"))t.classList.add(Fe)}_clearActiveClass(t){t.classList.remove(Fe);const e=U.find("[href].active",t);for(const t of e)t.classList.remove(Fe)}static jQueryInterface(t){return this.each((function(){const e=Be.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}$.on(window,"load.bs.scrollspy.data-api",(()=>{for(const t of U.find('[data-bs-spy="scroll"]'))Be.getOrCreateInstance(t)})),b(Be);const We="ArrowLeft",Re="ArrowRight",Ve="ArrowUp",Ke="ArrowDown",Qe="active",Xe="fade",Ye="show",Ue='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Ge=`.nav-link:not(.dropdown-toggle), .list-group-item:not(.dropdown-toggle), [role="tab"]:not(.dropdown-toggle), ${Ue}`;class Je extends V{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),$.on(this._element,"keydown.bs.tab",(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?$.trigger(e,"hide.bs.tab",{relatedTarget:t}):null;$.trigger(t,"show.bs.tab",{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Qe),this._activate(r(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.focus(),t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),$.trigger(t,"shown.bs.tab",{relatedTarget:e})):t.classList.add(Ye)}),t,t.classList.contains(Xe)))}_deactivate(t,e){t&&(t.classList.remove(Qe),t.blur(),this._deactivate(r(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),$.trigger(t,"hidden.bs.tab",{relatedTarget:e})):t.classList.remove(Ye)}),t,t.classList.contains(Xe)))}_keydown(t){if(![We,Re,Ve,Ke].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=[Re,Ke].includes(t.key),i=w(this._getChildren().filter((t=>!d(t))),t.target,e,!0);i&&Je.getOrCreateInstance(i).show()}_getChildren(){return U.find(Ge,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=r(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`#${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const s=(t,s)=>{const n=U.findOne(t,i);n&&n.classList.toggle(s,e)};s(".dropdown-toggle",Qe),s(".dropdown-menu",Ye),s(".dropdown-item",Qe),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Qe)}_getInnerElement(t){return t.matches(Ge)?t:U.findOne(Ge,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Je.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}$.on(document,"click.bs.tab",Ue,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),d(this)||Je.getOrCreateInstance(this).show()})),$.on(window,"load.bs.tab",(()=>{for(const t of U.find('.active[data-bs-toggle="tab"], .active[data-bs-toggle="pill"], .active[data-bs-toggle="list"]'))Je.getOrCreateInstance(t)})),b(Je);const Ze="hide",ti="show",ei="showing",ii={animation:"boolean",autohide:"boolean",delay:"number"},si={animation:!0,autohide:!0,delay:5e3};class ni extends V{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return si}static get DefaultType(){return ii}static get NAME(){return"toast"}show(){$.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Ze),g(this._element),this._element.classList.add(ti,ei),this._queueCallback((()=>{this._element.classList.remove(ei),$.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&($.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(ei),this._queueCallback((()=>{this._element.classList.add(Ze),this._element.classList.remove(ei,ti),$.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(ti),super.dispose()}isShown(){return this._element.classList.contains(ti)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){$.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),$.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),$.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),$.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ni.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return K(ni),b(ni),{Alert:Q,Button:Y,Carousel:ht,Collapse:mt,Dropdown:Mt,Modal:ne,Offcanvas:_e,Popover:je,ScrollSpy:Be,Tab:Je,Toast:ni,Tooltip:Pe}}));-//# sourceMappingURL=bootstrap.min.js.map
@@ -1,7 +0,0 @@-/*!- * Bootstrap v4.5.0 (https://getbootstrap.com/)- * Copyright 2011-2020 The Bootstrap Authors- * Copyright 2011-2020 Twitter, Inc.- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)- */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--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}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}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:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[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}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}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}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.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-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.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-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.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-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.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-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.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}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .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='%2328a745' 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(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.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 .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='%2328a745' 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") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' 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='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.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 .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' 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='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;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:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.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{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.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{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.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{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.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{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.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{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.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{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.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{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.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{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.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}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.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),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.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),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50%/50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.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='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.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='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.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='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff 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 .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.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){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.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")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.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")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.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}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item{display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.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}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.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:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.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:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50%/100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' 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")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' 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")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-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:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;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)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.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}}.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}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;-ms-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.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}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.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)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}-/*# sourceMappingURL=bootstrap.min.css.map */
@@ -1,1 +0,0 @@-{"version":3,"sources":["../../scss/bootstrap.scss","../../scss/_root.scss","../../scss/_reboot.scss","dist/css/bootstrap.css","../../scss/vendor/_rfs.scss","bootstrap.css","../../scss/mixins/_hover.scss","../../scss/_type.scss","../../scss/mixins/_lists.scss","../../scss/_images.scss","../../scss/mixins/_image.scss","../../scss/mixins/_border-radius.scss","../../scss/_code.scss","../../scss/_grid.scss","../../scss/mixins/_grid.scss","../../scss/mixins/_breakpoints.scss","../../scss/mixins/_grid-framework.scss","../../scss/_tables.scss","../../scss/mixins/_table-row.scss","../../scss/_forms.scss","../../scss/mixins/_transition.scss","../../scss/mixins/_forms.scss","../../scss/mixins/_gradients.scss","../../scss/_buttons.scss","../../scss/mixins/_buttons.scss","../../scss/_transitions.scss","../../scss/_dropdown.scss","../../scss/mixins/_caret.scss","../../scss/mixins/_nav-divider.scss","../../scss/_button-group.scss","../../scss/_input-group.scss","../../scss/_custom-forms.scss","../../scss/_nav.scss","../../scss/_navbar.scss","../../scss/_card.scss","../../scss/_breadcrumb.scss","../../scss/_pagination.scss","../../scss/mixins/_pagination.scss","../../scss/_badge.scss","../../scss/mixins/_badge.scss","../../scss/_jumbotron.scss","../../scss/_alert.scss","../../scss/mixins/_alert.scss","../../scss/_progress.scss","../../scss/_media.scss","../../scss/_list-group.scss","../../scss/mixins/_list-group.scss","../../scss/_close.scss","../../scss/_toasts.scss","../../scss/_modal.scss","../../scss/_tooltip.scss","../../scss/mixins/_reset-text.scss","../../scss/_popover.scss","../../scss/_carousel.scss","../../scss/mixins/_clearfix.scss","../../scss/_spinners.scss","../../scss/utilities/_align.scss","../../scss/mixins/_background-variant.scss","../../scss/utilities/_background.scss","../../scss/utilities/_borders.scss","../../scss/utilities/_display.scss","../../scss/utilities/_embed.scss","../../scss/utilities/_flex.scss","../../scss/utilities/_float.scss","../../scss/utilities/_interactions.scss","../../scss/utilities/_overflow.scss","../../scss/utilities/_position.scss","../../scss/utilities/_screenreaders.scss","../../scss/mixins/_screen-reader.scss","../../scss/utilities/_shadows.scss","../../scss/utilities/_sizing.scss","../../scss/utilities/_spacing.scss","../../scss/utilities/_stretched-link.scss","../../scss/utilities/_text.scss","../../scss/mixins/_text-truncate.scss","../../scss/mixins/_text-emphasis.scss","../../scss/mixins/_text-hide.scss","../../scss/utilities/_visibility.scss","../../scss/_print.scss"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Bootstrap v4.5.0 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"root\";\n@import \"reboot\";\n@import \"type\";\n@import \"images\";\n@import \"code\";\n@import \"grid\";\n@import \"tables\";\n@import \"forms\";\n@import \"buttons\";\n@import \"transitions\";\n@import \"dropdown\";\n@import \"button-group\";\n@import \"input-group\";\n@import \"custom-forms\";\n@import \"nav\";\n@import \"navbar\";\n@import \"card\";\n@import \"breadcrumb\";\n@import \"pagination\";\n@import \"badge\";\n@import \"jumbotron\";\n@import \"alert\";\n@import \"progress\";\n@import \"media\";\n@import \"list-group\";\n@import \"close\";\n@import \"toasts\";\n@import \"modal\";\n@import \"tooltip\";\n@import \"popover\";\n@import \"carousel\";\n@import \"spinners\";\n@import \"utilities\";\n@import \"print\";\n","// Do not forget to update getting-started/theming.md!\n:root {\n // Custom variable values only support SassScript inside `#{}`.\n @each $color, $value in $colors {\n --#{$color}: #{$value};\n }\n\n @each $color, $value in $theme-colors {\n --#{$color}: #{$value};\n }\n\n @each $bp, $value in $grid-breakpoints {\n --breakpoint-#{$bp}: #{$value};\n }\n\n // Use `inspect` for lists so that quoted items keep the quotes.\n // See https://github.com/sass/sass/issues/2383#issuecomment-336349172\n --font-family-sans-serif: #{inspect($font-family-sans-serif)};\n --font-family-monospace: #{inspect($font-family-monospace)};\n}\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -webkit-tap-highlight-color: rgba($black, 0); // 5\n}\n\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\n// TODO: remove in v5\n// stylelint-disable-next-line selector-list-comma-newline-after\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use\n// the `inherit` value on things like `<th>` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n @include font-size($font-size-base);\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline\n// on elements that programmatically receive focus but wouldn't normally show a visible\n// focus outline. In general, this would mean that the outline is only applied if the\n// interaction that led to the element receiving programmatic focus was a keyboard interaction,\n// or the browser has somehow determined that the user is primarily a keyboard user and/or\n// wants focus outlines to always be presented.\n//\n// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible\n// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `<h1>`-`<h6>` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable-next-line selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `<p>`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Duplicate behavior to the data-* attribute for our tooltip plugin\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Remove the bottom border in Firefox 39-.\n// 5. Prevent the text-decoration to be skipped.\n\nabbr[title],\nabbr[data-original-title] { // 1\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 4\n text-decoration-skip-ink: none; // 5\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n\nsmall {\n @include font-size(80%); // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n @include font-size(75%);\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n\n @include hover() {\n color: inherit;\n text-decoration: none;\n }\n}\n\n\n//\n// Code\n//\n\npre,\ncode,\nkbd,\nsamp {\n font-family: $font-family-monospace;\n @include font-size(1em); // Correct the odd `em` font sizing in all browsers.\n}\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // Disable auto-hiding scrollbar in IE & legacy Edge to avoid overlap,\n // making it impossible to interact with the content\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg {\n // Workaround for the SVG overflow bug in IE10/11 is still required.\n // See https://github.com/twbs/bootstrap/issues/26878\n overflow: hidden;\n vertical-align: middle;\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $table-caption-color;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `<td>` alignment by inheriting from the `<body>`, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: $label-margin-bottom;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n // stylelint-disable-next-line property-blacklist\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n @include font-size(inherit);\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// Set the cursor for non-`<button>` buttons\n//\n// Details at https://github.com/twbs/bootstrap/pull/30562\n[role=\"button\"] {\n cursor: pointer;\n}\n\n// Remove the inheritance of word-wrap in Safari.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24990\nselect {\n word-wrap: normal;\n}\n\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\n[type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Opinionated: add \"hand\" cursor to non-disabled button elements.\n@if $enable-pointer-cursor-for-buttons {\n button,\n [type=\"button\"],\n [type=\"reset\"],\n [type=\"submit\"] {\n &:not(:disabled) {\n cursor: pointer;\n }\n }\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `<div>`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n @include font-size(1.5rem);\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap v4.5.0 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --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\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n 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\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n -webkit-text-decoration-skip-ink: none;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=\"button\"] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-wrap: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.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,\n.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,\n.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,\n.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,\n.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,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n -ms-flex-order: -1;\n order: -1;\n}\n\n.order-last {\n -ms-flex-order: 13;\n order: 13;\n}\n\n.order-0 {\n -ms-flex-order: 0;\n order: 0;\n}\n\n.order-1 {\n -ms-flex-order: 1;\n order: 1;\n}\n\n.order-2 {\n -ms-flex-order: 2;\n order: 2;\n}\n\n.order-3 {\n -ms-flex-order: 3;\n order: 3;\n}\n\n.order-4 {\n -ms-flex-order: 4;\n order: 4;\n}\n\n.order-5 {\n -ms-flex-order: 5;\n order: 5;\n}\n\n.order-6 {\n -ms-flex-order: 6;\n order: 6;\n}\n\n.order-7 {\n -ms-flex-order: 7;\n order: 7;\n}\n\n.order-8 {\n -ms-flex-order: 8;\n order: 8;\n}\n\n.order-9 {\n -ms-flex-order: 9;\n order: 9;\n}\n\n.order-10 {\n -ms-flex-order: 10;\n order: 10;\n}\n\n.order-11 {\n -ms-flex-order: 11;\n order: 11;\n}\n\n.order-12 {\n -ms-flex-order: 12;\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-sm-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-sm-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-sm-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-sm-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-sm-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-sm-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-sm-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-sm-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-sm-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-sm-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-sm-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-sm-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-sm-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-sm-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-md-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-md-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-md-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-md-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-md-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-md-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-md-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-md-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-md-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-md-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-md-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-md-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-md-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-md-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-lg-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-lg-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-lg-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-lg-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-lg-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-lg-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-lg-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-lg-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-lg-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-lg-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-lg-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-lg-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-lg-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-lg-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n -ms-flex: 0 0 20%;\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n -ms-flex: 0 0 8.333333%;\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n -ms-flex: 0 0 16.666667%;\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n -ms-flex: 0 0 25%;\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n -ms-flex: 0 0 33.333333%;\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n -ms-flex: 0 0 41.666667%;\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n -ms-flex: 0 0 50%;\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n -ms-flex: 0 0 58.333333%;\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n -ms-flex: 0 0 66.666667%;\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n -ms-flex: 0 0 75%;\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n -ms-flex: 0 0 83.333333%;\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n -ms-flex: 0 0 91.666667%;\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n -ms-flex: 0 0 100%;\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n -ms-flex-order: -1;\n order: -1;\n }\n .order-xl-last {\n -ms-flex-order: 13;\n order: 13;\n }\n .order-xl-0 {\n -ms-flex-order: 0;\n order: 0;\n }\n .order-xl-1 {\n -ms-flex-order: 1;\n order: 1;\n }\n .order-xl-2 {\n -ms-flex-order: 2;\n order: 2;\n }\n .order-xl-3 {\n -ms-flex-order: 3;\n order: 3;\n }\n .order-xl-4 {\n -ms-flex-order: 4;\n order: 4;\n }\n .order-xl-5 {\n -ms-flex-order: 5;\n order: 5;\n }\n .order-xl-6 {\n -ms-flex-order: 6;\n order: 6;\n }\n .order-xl-7 {\n -ms-flex-order: 7;\n order: 7;\n }\n .order-xl-8 {\n -ms-flex-order: 8;\n order: 8;\n }\n .order-xl-9 {\n -ms-flex-order: 9;\n order: 9;\n }\n .order-xl-10 {\n -ms-flex-order: 10;\n order: 10;\n }\n .order-xl-11 {\n -ms-flex-order: 11;\n order: 11;\n }\n .order-xl-12 {\n -ms-flex-order: 12;\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n color: #212529;\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #343a40;\n border-color: #454d55;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #454d55;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::-webkit-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-moz-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::-ms-input-placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\ninput[type=\"date\"].form-control,\ninput[type=\"time\"].form-control,\ninput[type=\"datetime-local\"].form-control,\ninput[type=\"month\"].form-control {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n font-size: 1rem;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input[disabled] ~ .form-check-label,\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n -ms-flex-align: center;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: calc(1.5em + 0.75rem);\n 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='%2328a745' 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\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: calc(0.75em + 2.3125rem);\n 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='%2328a745' 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\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.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 {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' 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='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: calc(0.75em + 2.3125rem);\n 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='%23dc3545' 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='%23dc3545' stroke='none'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.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 {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n -ms-flex-align: center;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n -ms-flex-negative: 0;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n 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;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n text-decoration: none;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: -ms-inline-flexbox;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: center;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: stretch;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: -ms-flexbox;\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(1.5em + 1rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.5em + 0.5rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: -ms-inline-flexbox;\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n left: 0;\n z-index: -1;\n width: 1rem;\n height: 1.25rem;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background: no-repeat 50% / 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n 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='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n 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='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n 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='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n transition: 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;\n 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;\n 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;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n -webkit-transform: translateX(0.75rem);\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: #fff 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;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n display: none;\n}\n\n.custom-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.custom-select-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(1.5em + 1rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input[disabled] ~ .custom-file-label,\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: calc(1.5em + 0.75rem);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: 1.4rem;\n padding: 0;\n background-color: transparent;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -webkit-appearance: none;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n -webkit-transition: none;\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n -moz-appearance: none;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n -moz-transition: none;\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n -ms-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n -ms-transition: none;\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n -ms-flex-positive: 1;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar .container,\n.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: justify;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n -ms-flex-preferred-size: 100%;\n flex-basis: 100%;\n -ms-flex-positive: 1;\n flex-grow: 1;\n -ms-flex-align: center;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .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 {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .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 {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .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 {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .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 {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n -ms-flex-flow: row nowrap;\n flex-flow: row nowrap;\n -ms-flex-pack: start;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n -ms-flex-wrap: nowrap;\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: -ms-flexbox !important;\n display: flex !important;\n -ms-flex-preferred-size: auto;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n 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\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n 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\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-body {\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n min-height: 1px;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-flow: row wrap;\n flex-flow: row wrap;\n }\n .card-group > .card {\n -ms-flex: 1 0 0%;\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n -webkit-column-count: 3;\n -moz-column-count: 3;\n column-count: 3;\n -webkit-column-gap: 1.25rem;\n -moz-column-gap: 1.25rem;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion > .card {\n overflow: hidden;\n}\n\n.accordion > .card:not(:last-of-type) {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion > .card:not(:first-of-type) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion > .card > .card-header {\n border-radius: 0;\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item {\n display: -ms-flexbox;\n display: flex;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: -ms-flexbox;\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 3;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n 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;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .badge {\n transition: none;\n }\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\na.badge-primary:focus, a.badge-primary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\na.badge-secondary:focus, a.badge-secondary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\na.badge-success:focus, a.badge-success.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\na.badge-info:focus, a.badge-info.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\na.badge-warning:focus, a.badge-warning.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\na.badge-danger:focus, a.badge-danger.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\na.badge-light:focus, a.badge-light.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\na.badge-dark:focus, a.badge-dark.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: -ms-flexbox;\n display: flex;\n height: 1rem;\n overflow: hidden;\n line-height: 0;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-pack: center;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n 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);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n -webkit-animation: progress-bar-stripes 1s linear infinite;\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n -webkit-animation: none;\n animation: none;\n }\n}\n\n.media {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n}\n\n.media-body {\n -ms-flex: 1;\n flex: 1;\n}\n\n.list-group {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: 0.25rem;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n -ms-flex-direction: row;\n flex-direction: row;\n}\n\n.list-group-horizontal > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n\n.list-group-horizontal > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n -ms-flex-direction: row;\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n.list-group-flush {\n border-radius: 0;\n}\n\n.list-group-flush > .list-group-item {\n border-width: 0 0 1px;\n}\n\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n -webkit-backdrop-filter: blur(10px);\n backdrop-filter: blur(10px);\n opacity: 0;\n border-radius: 0.25rem;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: -webkit-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;\n -webkit-transform: translate(0, -50px);\n transform: translate(0, -50px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n -webkit-transform: none;\n transform: none;\n}\n\n.modal.modal-static .modal-dialog {\n -webkit-transform: scale(1.02);\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n display: -ms-flexbox;\n display: flex;\n max-height: calc(100% - 1rem);\n}\n\n.modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 1rem);\n overflow: hidden;\n}\n\n.modal-dialog-scrollable .modal-header,\n.modal-dialog-scrollable .modal-footer {\n -ms-flex-negative: 0;\n flex-shrink: 0;\n}\n\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - 1rem);\n height: -webkit-min-content;\n height: -moz-min-content;\n height: min-content;\n content: \"\";\n}\n\n.modal-dialog-centered.modal-dialog-scrollable {\n -ms-flex-direction: column;\n flex-direction: column;\n -ms-flex-pack: center;\n justify-content: center;\n height: 100%;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable .modal-content {\n max-height: none;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable::before {\n content: none;\n}\n\n.modal-content {\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: start;\n align-items: flex-start;\n -ms-flex-pack: justify;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-wrap: wrap;\n flex-wrap: wrap;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: end;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-scrollable {\n max-height: calc(100% - 3.5rem);\n }\n .modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 3.5rem);\n }\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n .modal-dialog-centered::before {\n height: calc(100vh - 3.5rem);\n height: -webkit-min-content;\n height: -moz-min-content;\n height: min-content;\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n 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\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n 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\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=\"top\"] > .arrow {\n bottom: calc(-0.5rem - 1px);\n}\n\n.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=\"top\"] > .arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=\"top\"] > .arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=\"right\"] > .arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=\"right\"] > .arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=\"right\"] > .arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow {\n top: calc(-0.5rem - 1px);\n}\n\n.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=\"left\"] > .arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=\"left\"] > .arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=\"left\"] > .arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n -ms-touch-action: pan-y;\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n transition: -webkit-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n -webkit-transform: translateX(100%);\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n -webkit-transform: translateX(-100%);\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n -webkit-transform: none;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-align: center;\n align-items: center;\n -ms-flex-pack: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: no-repeat 50% / 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' 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\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' 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\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: -ms-flexbox;\n display: flex;\n -ms-flex-pack: center;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n -ms-flex: 0 1 auto;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@-webkit-keyframes spinner-border {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n@keyframes spinner-border {\n to {\n -webkit-transform: rotate(360deg);\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n -webkit-animation: spinner-border .75s linear infinite;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@-webkit-keyframes spinner-grow {\n 0% {\n -webkit-transform: scale(0);\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n -webkit-transform: none;\n transform: none;\n }\n}\n\n@keyframes spinner-grow {\n 0% {\n -webkit-transform: scale(0);\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n -webkit-transform: none;\n transform: none;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n -webkit-animation: spinner-grow .75s linear infinite;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded-sm {\n border-radius: 0.2rem !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-lg {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n}\n\n.d-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-md-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: -ms-flexbox !important;\n display: flex !important;\n }\n .d-print-inline-flex {\n display: -ms-inline-flexbox !important;\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n}\n\n.flex-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n}\n\n.justify-content-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n}\n\n.align-items-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n}\n\n.align-items-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n}\n\n.align-items-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n}\n\n.align-items-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n}\n\n.align-content-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n}\n\n.align-content-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n}\n\n.align-content-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n}\n\n.align-content-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n}\n\n.align-content-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n}\n\n.align-self-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n}\n\n.align-self-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n}\n\n.align-self-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n}\n\n.align-self-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n}\n\n.align-self-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-sm-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-sm-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-sm-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-sm-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-sm-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-sm-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-sm-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-sm-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-md-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-md-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-md-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-md-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-md-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-md-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-md-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-md-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-md-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-md-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-md-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-md-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-md-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-md-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-md-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-md-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-lg-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-lg-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-lg-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-lg-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-lg-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-lg-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-lg-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-lg-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n -ms-flex-direction: row !important;\n flex-direction: row !important;\n }\n .flex-xl-column {\n -ms-flex-direction: column !important;\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n -ms-flex-direction: row-reverse !important;\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n -ms-flex-direction: column-reverse !important;\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n -ms-flex-wrap: wrap !important;\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n -ms-flex-wrap: nowrap !important;\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n -ms-flex-wrap: wrap-reverse !important;\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n -ms-flex: 1 1 auto !important;\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n -ms-flex-positive: 0 !important;\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n -ms-flex-positive: 1 !important;\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n -ms-flex-negative: 0 !important;\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n -ms-flex-negative: 1 !important;\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n -ms-flex-pack: start !important;\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n -ms-flex-pack: end !important;\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n -ms-flex-pack: center !important;\n justify-content: center !important;\n }\n .justify-content-xl-between {\n -ms-flex-pack: justify !important;\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n -ms-flex-pack: distribute !important;\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n -ms-flex-align: start !important;\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n -ms-flex-align: end !important;\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n -ms-flex-align: center !important;\n align-items: center !important;\n }\n .align-items-xl-baseline {\n -ms-flex-align: baseline !important;\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n -ms-flex-align: stretch !important;\n align-items: stretch !important;\n }\n .align-content-xl-start {\n -ms-flex-line-pack: start !important;\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n -ms-flex-line-pack: end !important;\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n -ms-flex-line-pack: center !important;\n align-content: center !important;\n }\n .align-content-xl-between {\n -ms-flex-line-pack: justify !important;\n align-content: space-between !important;\n }\n .align-content-xl-around {\n -ms-flex-line-pack: distribute !important;\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n -ms-flex-line-pack: stretch !important;\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n -ms-flex-item-align: auto !important;\n align-self: auto !important;\n }\n .align-self-xl-start {\n -ms-flex-item-align: start !important;\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n -ms-flex-item-align: end !important;\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n -ms-flex-item-align: center !important;\n align-self: center !important;\n }\n .align-self-xl-baseline {\n -ms-flex-item-align: baseline !important;\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n -ms-flex-item-align: stretch !important;\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.user-select-all {\n -webkit-user-select: all !important;\n -moz-user-select: all !important;\n -ms-user-select: all !important;\n user-select: all !important;\n}\n\n.user-select-auto {\n -webkit-user-select: auto !important;\n -moz-user-select: auto !important;\n -ms-user-select: auto !important;\n user-select: auto !important;\n}\n\n.user-select-none {\n -webkit-user-select: none !important;\n -moz-user-select: none !important;\n -ms-user-select: none !important;\n user-select: none !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: -webkit-sticky !important;\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports ((position: -webkit-sticky) or (position: sticky)) {\n .sticky-top {\n position: -webkit-sticky;\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n pointer-events: auto;\n content: \"\";\n background-color: rgba(0, 0, 0, 0);\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-break {\n word-wrap: break-word !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","// stylelint-disable property-blacklist, scss/dollar-variable-default\n\n// SCSS RFS mixin\n//\n// Automated font-resizing\n//\n// See https://github.com/twbs/rfs\n\n// Configuration\n\n// Base font size\n$rfs-base-font-size: 1.25rem !default;\n$rfs-font-size-unit: rem !default;\n\n// Breakpoint at where font-size starts decreasing if screen width is smaller\n$rfs-breakpoint: 1200px !default;\n$rfs-breakpoint-unit: px !default;\n\n// Resize font-size based on screen height and width\n$rfs-two-dimensional: false !default;\n\n// Factor of decrease\n$rfs-factor: 10 !default;\n\n@if type-of($rfs-factor) != \"number\" or $rfs-factor <= 1 {\n @error \"`#{$rfs-factor}` is not a valid $rfs-factor, it must be greater than 1.\";\n}\n\n// Generate enable or disable classes. Possibilities: false, \"enable\" or \"disable\"\n$rfs-class: false !default;\n\n// 1 rem = $rfs-rem-value px\n$rfs-rem-value: 16 !default;\n\n// Safari iframe resize bug: https://github.com/twbs/rfs/issues/14\n$rfs-safari-iframe-resize-bug-fix: false !default;\n\n// Disable RFS by setting $enable-responsive-font-sizes to false\n$enable-responsive-font-sizes: true !default;\n\n// Cache $rfs-base-font-size unit\n$rfs-base-font-size-unit: unit($rfs-base-font-size);\n\n// Remove px-unit from $rfs-base-font-size for calculations\n@if $rfs-base-font-size-unit == \"px\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1);\n}\n@else if $rfs-base-font-size-unit == \"rem\" {\n $rfs-base-font-size: $rfs-base-font-size / ($rfs-base-font-size * 0 + 1 / $rfs-rem-value);\n}\n\n// Cache $rfs-breakpoint unit to prevent multiple calls\n$rfs-breakpoint-unit-cache: unit($rfs-breakpoint);\n\n// Remove unit from $rfs-breakpoint for calculations\n@if $rfs-breakpoint-unit-cache == \"px\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1);\n}\n@else if $rfs-breakpoint-unit-cache == \"rem\" or $rfs-breakpoint-unit-cache == \"em\" {\n $rfs-breakpoint: $rfs-breakpoint / ($rfs-breakpoint * 0 + 1 / $rfs-rem-value);\n}\n\n// Responsive font-size mixin\n@mixin rfs($fs, $important: false) {\n // Cache $fs unit\n $fs-unit: if(type-of($fs) == \"number\", unit($fs), false);\n\n // Add !important suffix if needed\n $rfs-suffix: if($important, \" !important\", \"\");\n\n // If $fs isn't a number (like inherit) or $fs has a unit (not px or rem, like 1.5em) or $ is 0, just print the value\n @if not $fs-unit or $fs-unit != \"\" and $fs-unit != \"px\" and $fs-unit != \"rem\" or $fs == 0 {\n font-size: #{$fs}#{$rfs-suffix};\n }\n @else {\n // Variables for storing static and fluid rescaling\n $rfs-static: null;\n $rfs-fluid: null;\n\n // Remove px-unit from $fs for calculations\n @if $fs-unit == \"px\" {\n $fs: $fs / ($fs * 0 + 1);\n }\n @else if $fs-unit == \"rem\" {\n $fs: $fs / ($fs * 0 + 1 / $rfs-rem-value);\n }\n\n // Set default font-size\n @if $rfs-font-size-unit == rem {\n $rfs-static: #{$fs / $rfs-rem-value}rem#{$rfs-suffix};\n }\n @else if $rfs-font-size-unit == px {\n $rfs-static: #{$fs}px#{$rfs-suffix};\n }\n @else {\n @error \"`#{$rfs-font-size-unit}` is not a valid unit for $rfs-font-size-unit. Use `px` or `rem`.\";\n }\n\n // Only add media query if font-size is bigger as the minimum font-size\n // If $rfs-factor == 1, no rescaling will take place\n @if $fs > $rfs-base-font-size and $enable-responsive-font-sizes {\n $min-width: null;\n $variable-unit: null;\n\n // Calculate minimum font-size for given font-size\n $fs-min: $rfs-base-font-size + ($fs - $rfs-base-font-size) / $rfs-factor;\n\n // Calculate difference between given font-size and minimum font-size for given font-size\n $fs-diff: $fs - $fs-min;\n\n // Base font-size formatting\n // No need to check if the unit is valid, because we did that before\n $min-width: if($rfs-font-size-unit == rem, #{$fs-min / $rfs-rem-value}rem, #{$fs-min}px);\n\n // If two-dimensional, use smallest of screen width and height\n $variable-unit: if($rfs-two-dimensional, vmin, vw);\n\n // Calculate the variable width between 0 and $rfs-breakpoint\n $variable-width: #{$fs-diff * 100 / $rfs-breakpoint}#{$variable-unit};\n\n // Set the calculated font-size.\n $rfs-fluid: calc(#{$min-width} + #{$variable-width}) #{$rfs-suffix};\n }\n\n // Rendering\n @if $rfs-fluid == null {\n // Only render static font-size if no fluid font-size is available\n font-size: $rfs-static;\n }\n @else {\n $mq-value: null;\n\n // RFS breakpoint formatting\n @if $rfs-breakpoint-unit == em or $rfs-breakpoint-unit == rem {\n $mq-value: #{$rfs-breakpoint / $rfs-rem-value}#{$rfs-breakpoint-unit};\n }\n @else if $rfs-breakpoint-unit == px {\n $mq-value: #{$rfs-breakpoint}px;\n }\n @else {\n @error \"`#{$rfs-breakpoint-unit}` is not a valid unit for $rfs-breakpoint-unit. Use `px`, `em` or `rem`.\";\n }\n\n @if $rfs-class == \"disable\" {\n // Adding an extra class increases specificity,\n // which prevents the media query to override the font size\n &,\n .disable-responsive-font-size &,\n &.disable-responsive-font-size {\n font-size: $rfs-static;\n }\n }\n @else {\n font-size: $rfs-static;\n }\n\n @if $rfs-two-dimensional {\n @media (max-width: #{$mq-value}), (max-height: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n @else {\n @media (max-width: #{$mq-value}) {\n @if $rfs-class == \"enable\" {\n .enable-responsive-font-size &,\n &.enable-responsive-font-size {\n font-size: $rfs-fluid;\n }\n }\n @else {\n font-size: $rfs-fluid;\n }\n\n @if $rfs-safari-iframe-resize-bug-fix {\n // stylelint-disable-next-line length-zero-no-unit\n min-width: 0vw;\n }\n }\n }\n }\n }\n}\n\n// The font-size & responsive-font-size mixin uses RFS to rescale font sizes\n@mixin font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n\n@mixin responsive-font-size($fs, $important: false) {\n @include rfs($fs, $important);\n}\n","/*!\n * Bootstrap v4.5.0 (https://getbootstrap.com/)\n * Copyright 2011-2020 The Bootstrap Authors\n * Copyright 2011-2020 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n:root {\n --blue: #007bff;\n --indigo: #6610f2;\n --purple: #6f42c1;\n --pink: #e83e8c;\n --red: #dc3545;\n --orange: #fd7e14;\n --yellow: #ffc107;\n --green: #28a745;\n --teal: #20c997;\n --cyan: #17a2b8;\n --white: #fff;\n --gray: #6c757d;\n --gray-dark: #343a40;\n --primary: #007bff;\n --secondary: #6c757d;\n --success: #28a745;\n --info: #17a2b8;\n --warning: #ffc107;\n --danger: #dc3545;\n --light: #f8f9fa;\n --dark: #343a40;\n --breakpoint-xs: 0;\n --breakpoint-sm: 576px;\n --breakpoint-md: 768px;\n --breakpoint-lg: 992px;\n --breakpoint-xl: 1200px;\n --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\";\n --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\narticle, aside, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n 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\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus:not(:focus-visible) {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n text-decoration-skip-ink: none;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):hover {\n color: inherit;\n text-decoration: none;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg {\n overflow: hidden;\n vertical-align: middle;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: 0.5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\n[role=\"button\"] {\n cursor: pointer;\n}\n\nselect {\n word-wrap: normal;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton:not(:disabled),\n[type=\"button\"]:not(:disabled),\n[type=\"reset\"]:not(:disabled),\n[type=\"submit\"]:not(:disabled) {\n cursor: pointer;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: 0.5rem;\n font-weight: 500;\n line-height: 1.2;\n}\n\nh1, .h1 {\n font-size: 2.5rem;\n}\n\nh2, .h2 {\n font-size: 2rem;\n}\n\nh3, .h3 {\n font-size: 1.75rem;\n}\n\nh4, .h4 {\n font-size: 1.5rem;\n}\n\nh5, .h5 {\n font-size: 1.25rem;\n}\n\nh6, .h6 {\n font-size: 1rem;\n}\n\n.lead {\n font-size: 1.25rem;\n font-weight: 300;\n}\n\n.display-1 {\n font-size: 6rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-2 {\n font-size: 5.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-3 {\n font-size: 4.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\n.display-4 {\n font-size: 3.5rem;\n font-weight: 300;\n line-height: 1.2;\n}\n\nhr {\n margin-top: 1rem;\n margin-bottom: 1rem;\n border: 0;\n border-top: 1px solid rgba(0, 0, 0, 0.1);\n}\n\nsmall,\n.small {\n font-size: 80%;\n font-weight: 400;\n}\n\nmark,\n.mark {\n padding: 0.2em;\n background-color: #fcf8e3;\n}\n\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline {\n padding-left: 0;\n list-style: none;\n}\n\n.list-inline-item {\n display: inline-block;\n}\n\n.list-inline-item:not(:last-child) {\n margin-right: 0.5rem;\n}\n\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n.blockquote {\n margin-bottom: 1rem;\n font-size: 1.25rem;\n}\n\n.blockquote-footer {\n display: block;\n font-size: 80%;\n color: #6c757d;\n}\n\n.blockquote-footer::before {\n content: \"\\2014\\00A0\";\n}\n\n.img-fluid {\n max-width: 100%;\n height: auto;\n}\n\n.img-thumbnail {\n padding: 0.25rem;\n background-color: #fff;\n border: 1px solid #dee2e6;\n border-radius: 0.25rem;\n max-width: 100%;\n height: auto;\n}\n\n.figure {\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: 0.5rem;\n line-height: 1;\n}\n\n.figure-caption {\n font-size: 90%;\n color: #6c757d;\n}\n\ncode {\n font-size: 87.5%;\n color: #e83e8c;\n word-wrap: break-word;\n}\n\na > code {\n color: inherit;\n}\n\nkbd {\n padding: 0.2rem 0.4rem;\n font-size: 87.5%;\n color: #fff;\n background-color: #212529;\n border-radius: 0.2rem;\n}\n\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: 700;\n}\n\npre {\n display: block;\n font-size: 87.5%;\n color: #212529;\n}\n\npre code {\n font-size: inherit;\n color: inherit;\n word-break: normal;\n}\n\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n\n.container {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container {\n max-width: 1140px;\n }\n}\n\n.container-fluid, .container-sm, .container-md, .container-lg, .container-xl {\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n\n@media (min-width: 576px) {\n .container, .container-sm {\n max-width: 540px;\n }\n}\n\n@media (min-width: 768px) {\n .container, .container-sm, .container-md {\n max-width: 720px;\n }\n}\n\n@media (min-width: 992px) {\n .container, .container-sm, .container-md, .container-lg {\n max-width: 960px;\n }\n}\n\n@media (min-width: 1200px) {\n .container, .container-sm, .container-md, .container-lg, .container-xl {\n max-width: 1140px;\n }\n}\n\n.row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -15px;\n margin-left: -15px;\n}\n\n.no-gutters {\n margin-right: 0;\n margin-left: 0;\n}\n\n.no-gutters > .col,\n.no-gutters > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n}\n\n.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,\n.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,\n.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,\n.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,\n.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,\n.col-xl-auto {\n position: relative;\n width: 100%;\n padding-right: 15px;\n padding-left: 15px;\n}\n\n.col {\n flex-basis: 0;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n}\n\n.row-cols-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.row-cols-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.row-cols-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.row-cols-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.row-cols-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n}\n\n.row-cols-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n}\n\n.col-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n}\n\n.col-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n}\n\n.col-3 {\n flex: 0 0 25%;\n max-width: 25%;\n}\n\n.col-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n}\n\n.col-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n}\n\n.col-6 {\n flex: 0 0 50%;\n max-width: 50%;\n}\n\n.col-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n}\n\n.col-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n}\n\n.col-9 {\n flex: 0 0 75%;\n max-width: 75%;\n}\n\n.col-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n}\n\n.col-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n}\n\n.col-12 {\n flex: 0 0 100%;\n max-width: 100%;\n}\n\n.order-first {\n order: -1;\n}\n\n.order-last {\n order: 13;\n}\n\n.order-0 {\n order: 0;\n}\n\n.order-1 {\n order: 1;\n}\n\n.order-2 {\n order: 2;\n}\n\n.order-3 {\n order: 3;\n}\n\n.order-4 {\n order: 4;\n}\n\n.order-5 {\n order: 5;\n}\n\n.order-6 {\n order: 6;\n}\n\n.order-7 {\n order: 7;\n}\n\n.order-8 {\n order: 8;\n}\n\n.order-9 {\n order: 9;\n}\n\n.order-10 {\n order: 10;\n}\n\n.order-11 {\n order: 11;\n}\n\n.order-12 {\n order: 12;\n}\n\n.offset-1 {\n margin-left: 8.333333%;\n}\n\n.offset-2 {\n margin-left: 16.666667%;\n}\n\n.offset-3 {\n margin-left: 25%;\n}\n\n.offset-4 {\n margin-left: 33.333333%;\n}\n\n.offset-5 {\n margin-left: 41.666667%;\n}\n\n.offset-6 {\n margin-left: 50%;\n}\n\n.offset-7 {\n margin-left: 58.333333%;\n}\n\n.offset-8 {\n margin-left: 66.666667%;\n}\n\n.offset-9 {\n margin-left: 75%;\n}\n\n.offset-10 {\n margin-left: 83.333333%;\n}\n\n.offset-11 {\n margin-left: 91.666667%;\n}\n\n@media (min-width: 576px) {\n .col-sm {\n flex-basis: 0;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-sm-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-sm-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-sm-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-sm-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-sm-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-sm-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-sm-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-sm-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-sm-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-sm-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-sm-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-sm-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-sm-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-sm-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-sm-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-sm-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-sm-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-sm-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-sm-first {\n order: -1;\n }\n .order-sm-last {\n order: 13;\n }\n .order-sm-0 {\n order: 0;\n }\n .order-sm-1 {\n order: 1;\n }\n .order-sm-2 {\n order: 2;\n }\n .order-sm-3 {\n order: 3;\n }\n .order-sm-4 {\n order: 4;\n }\n .order-sm-5 {\n order: 5;\n }\n .order-sm-6 {\n order: 6;\n }\n .order-sm-7 {\n order: 7;\n }\n .order-sm-8 {\n order: 8;\n }\n .order-sm-9 {\n order: 9;\n }\n .order-sm-10 {\n order: 10;\n }\n .order-sm-11 {\n order: 11;\n }\n .order-sm-12 {\n order: 12;\n }\n .offset-sm-0 {\n margin-left: 0;\n }\n .offset-sm-1 {\n margin-left: 8.333333%;\n }\n .offset-sm-2 {\n margin-left: 16.666667%;\n }\n .offset-sm-3 {\n margin-left: 25%;\n }\n .offset-sm-4 {\n margin-left: 33.333333%;\n }\n .offset-sm-5 {\n margin-left: 41.666667%;\n }\n .offset-sm-6 {\n margin-left: 50%;\n }\n .offset-sm-7 {\n margin-left: 58.333333%;\n }\n .offset-sm-8 {\n margin-left: 66.666667%;\n }\n .offset-sm-9 {\n margin-left: 75%;\n }\n .offset-sm-10 {\n margin-left: 83.333333%;\n }\n .offset-sm-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 768px) {\n .col-md {\n flex-basis: 0;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-md-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-md-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-md-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-md-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-md-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-md-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-md-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-md-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-md-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-md-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-md-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-md-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-md-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-md-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-md-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-md-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-md-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-md-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-md-first {\n order: -1;\n }\n .order-md-last {\n order: 13;\n }\n .order-md-0 {\n order: 0;\n }\n .order-md-1 {\n order: 1;\n }\n .order-md-2 {\n order: 2;\n }\n .order-md-3 {\n order: 3;\n }\n .order-md-4 {\n order: 4;\n }\n .order-md-5 {\n order: 5;\n }\n .order-md-6 {\n order: 6;\n }\n .order-md-7 {\n order: 7;\n }\n .order-md-8 {\n order: 8;\n }\n .order-md-9 {\n order: 9;\n }\n .order-md-10 {\n order: 10;\n }\n .order-md-11 {\n order: 11;\n }\n .order-md-12 {\n order: 12;\n }\n .offset-md-0 {\n margin-left: 0;\n }\n .offset-md-1 {\n margin-left: 8.333333%;\n }\n .offset-md-2 {\n margin-left: 16.666667%;\n }\n .offset-md-3 {\n margin-left: 25%;\n }\n .offset-md-4 {\n margin-left: 33.333333%;\n }\n .offset-md-5 {\n margin-left: 41.666667%;\n }\n .offset-md-6 {\n margin-left: 50%;\n }\n .offset-md-7 {\n margin-left: 58.333333%;\n }\n .offset-md-8 {\n margin-left: 66.666667%;\n }\n .offset-md-9 {\n margin-left: 75%;\n }\n .offset-md-10 {\n margin-left: 83.333333%;\n }\n .offset-md-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 992px) {\n .col-lg {\n flex-basis: 0;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-lg-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-lg-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-lg-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-lg-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-lg-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-lg-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-lg-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-lg-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-lg-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-lg-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-lg-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-lg-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-lg-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-lg-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-lg-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-lg-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-lg-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-lg-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-lg-first {\n order: -1;\n }\n .order-lg-last {\n order: 13;\n }\n .order-lg-0 {\n order: 0;\n }\n .order-lg-1 {\n order: 1;\n }\n .order-lg-2 {\n order: 2;\n }\n .order-lg-3 {\n order: 3;\n }\n .order-lg-4 {\n order: 4;\n }\n .order-lg-5 {\n order: 5;\n }\n .order-lg-6 {\n order: 6;\n }\n .order-lg-7 {\n order: 7;\n }\n .order-lg-8 {\n order: 8;\n }\n .order-lg-9 {\n order: 9;\n }\n .order-lg-10 {\n order: 10;\n }\n .order-lg-11 {\n order: 11;\n }\n .order-lg-12 {\n order: 12;\n }\n .offset-lg-0 {\n margin-left: 0;\n }\n .offset-lg-1 {\n margin-left: 8.333333%;\n }\n .offset-lg-2 {\n margin-left: 16.666667%;\n }\n .offset-lg-3 {\n margin-left: 25%;\n }\n .offset-lg-4 {\n margin-left: 33.333333%;\n }\n .offset-lg-5 {\n margin-left: 41.666667%;\n }\n .offset-lg-6 {\n margin-left: 50%;\n }\n .offset-lg-7 {\n margin-left: 58.333333%;\n }\n .offset-lg-8 {\n margin-left: 66.666667%;\n }\n .offset-lg-9 {\n margin-left: 75%;\n }\n .offset-lg-10 {\n margin-left: 83.333333%;\n }\n .offset-lg-11 {\n margin-left: 91.666667%;\n }\n}\n\n@media (min-width: 1200px) {\n .col-xl {\n flex-basis: 0;\n flex-grow: 1;\n min-width: 0;\n max-width: 100%;\n }\n .row-cols-xl-1 > * {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .row-cols-xl-2 > * {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .row-cols-xl-3 > * {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .row-cols-xl-4 > * {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .row-cols-xl-5 > * {\n flex: 0 0 20%;\n max-width: 20%;\n }\n .row-cols-xl-6 > * {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-auto {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%;\n }\n .col-xl-1 {\n flex: 0 0 8.333333%;\n max-width: 8.333333%;\n }\n .col-xl-2 {\n flex: 0 0 16.666667%;\n max-width: 16.666667%;\n }\n .col-xl-3 {\n flex: 0 0 25%;\n max-width: 25%;\n }\n .col-xl-4 {\n flex: 0 0 33.333333%;\n max-width: 33.333333%;\n }\n .col-xl-5 {\n flex: 0 0 41.666667%;\n max-width: 41.666667%;\n }\n .col-xl-6 {\n flex: 0 0 50%;\n max-width: 50%;\n }\n .col-xl-7 {\n flex: 0 0 58.333333%;\n max-width: 58.333333%;\n }\n .col-xl-8 {\n flex: 0 0 66.666667%;\n max-width: 66.666667%;\n }\n .col-xl-9 {\n flex: 0 0 75%;\n max-width: 75%;\n }\n .col-xl-10 {\n flex: 0 0 83.333333%;\n max-width: 83.333333%;\n }\n .col-xl-11 {\n flex: 0 0 91.666667%;\n max-width: 91.666667%;\n }\n .col-xl-12 {\n flex: 0 0 100%;\n max-width: 100%;\n }\n .order-xl-first {\n order: -1;\n }\n .order-xl-last {\n order: 13;\n }\n .order-xl-0 {\n order: 0;\n }\n .order-xl-1 {\n order: 1;\n }\n .order-xl-2 {\n order: 2;\n }\n .order-xl-3 {\n order: 3;\n }\n .order-xl-4 {\n order: 4;\n }\n .order-xl-5 {\n order: 5;\n }\n .order-xl-6 {\n order: 6;\n }\n .order-xl-7 {\n order: 7;\n }\n .order-xl-8 {\n order: 8;\n }\n .order-xl-9 {\n order: 9;\n }\n .order-xl-10 {\n order: 10;\n }\n .order-xl-11 {\n order: 11;\n }\n .order-xl-12 {\n order: 12;\n }\n .offset-xl-0 {\n margin-left: 0;\n }\n .offset-xl-1 {\n margin-left: 8.333333%;\n }\n .offset-xl-2 {\n margin-left: 16.666667%;\n }\n .offset-xl-3 {\n margin-left: 25%;\n }\n .offset-xl-4 {\n margin-left: 33.333333%;\n }\n .offset-xl-5 {\n margin-left: 41.666667%;\n }\n .offset-xl-6 {\n margin-left: 50%;\n }\n .offset-xl-7 {\n margin-left: 58.333333%;\n }\n .offset-xl-8 {\n margin-left: 66.666667%;\n }\n .offset-xl-9 {\n margin-left: 75%;\n }\n .offset-xl-10 {\n margin-left: 83.333333%;\n }\n .offset-xl-11 {\n margin-left: 91.666667%;\n }\n}\n\n.table {\n width: 100%;\n margin-bottom: 1rem;\n color: #212529;\n}\n\n.table th,\n.table td {\n padding: 0.75rem;\n vertical-align: top;\n border-top: 1px solid #dee2e6;\n}\n\n.table thead th {\n vertical-align: bottom;\n border-bottom: 2px solid #dee2e6;\n}\n\n.table tbody + tbody {\n border-top: 2px solid #dee2e6;\n}\n\n.table-sm th,\n.table-sm td {\n padding: 0.3rem;\n}\n\n.table-bordered {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered th,\n.table-bordered td {\n border: 1px solid #dee2e6;\n}\n\n.table-bordered thead th,\n.table-bordered thead td {\n border-bottom-width: 2px;\n}\n\n.table-borderless th,\n.table-borderless td,\n.table-borderless thead th,\n.table-borderless tbody + tbody {\n border: 0;\n}\n\n.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(0, 0, 0, 0.05);\n}\n\n.table-hover tbody tr:hover {\n color: #212529;\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-primary,\n.table-primary > th,\n.table-primary > td {\n background-color: #b8daff;\n}\n\n.table-primary th,\n.table-primary td,\n.table-primary thead th,\n.table-primary tbody + tbody {\n border-color: #7abaff;\n}\n\n.table-hover .table-primary:hover {\n background-color: #9fcdff;\n}\n\n.table-hover .table-primary:hover > td,\n.table-hover .table-primary:hover > th {\n background-color: #9fcdff;\n}\n\n.table-secondary,\n.table-secondary > th,\n.table-secondary > td {\n background-color: #d6d8db;\n}\n\n.table-secondary th,\n.table-secondary td,\n.table-secondary thead th,\n.table-secondary tbody + tbody {\n border-color: #b3b7bb;\n}\n\n.table-hover .table-secondary:hover {\n background-color: #c8cbcf;\n}\n\n.table-hover .table-secondary:hover > td,\n.table-hover .table-secondary:hover > th {\n background-color: #c8cbcf;\n}\n\n.table-success,\n.table-success > th,\n.table-success > td {\n background-color: #c3e6cb;\n}\n\n.table-success th,\n.table-success td,\n.table-success thead th,\n.table-success tbody + tbody {\n border-color: #8fd19e;\n}\n\n.table-hover .table-success:hover {\n background-color: #b1dfbb;\n}\n\n.table-hover .table-success:hover > td,\n.table-hover .table-success:hover > th {\n background-color: #b1dfbb;\n}\n\n.table-info,\n.table-info > th,\n.table-info > td {\n background-color: #bee5eb;\n}\n\n.table-info th,\n.table-info td,\n.table-info thead th,\n.table-info tbody + tbody {\n border-color: #86cfda;\n}\n\n.table-hover .table-info:hover {\n background-color: #abdde5;\n}\n\n.table-hover .table-info:hover > td,\n.table-hover .table-info:hover > th {\n background-color: #abdde5;\n}\n\n.table-warning,\n.table-warning > th,\n.table-warning > td {\n background-color: #ffeeba;\n}\n\n.table-warning th,\n.table-warning td,\n.table-warning thead th,\n.table-warning tbody + tbody {\n border-color: #ffdf7e;\n}\n\n.table-hover .table-warning:hover {\n background-color: #ffe8a1;\n}\n\n.table-hover .table-warning:hover > td,\n.table-hover .table-warning:hover > th {\n background-color: #ffe8a1;\n}\n\n.table-danger,\n.table-danger > th,\n.table-danger > td {\n background-color: #f5c6cb;\n}\n\n.table-danger th,\n.table-danger td,\n.table-danger thead th,\n.table-danger tbody + tbody {\n border-color: #ed969e;\n}\n\n.table-hover .table-danger:hover {\n background-color: #f1b0b7;\n}\n\n.table-hover .table-danger:hover > td,\n.table-hover .table-danger:hover > th {\n background-color: #f1b0b7;\n}\n\n.table-light,\n.table-light > th,\n.table-light > td {\n background-color: #fdfdfe;\n}\n\n.table-light th,\n.table-light td,\n.table-light thead th,\n.table-light tbody + tbody {\n border-color: #fbfcfc;\n}\n\n.table-hover .table-light:hover {\n background-color: #ececf6;\n}\n\n.table-hover .table-light:hover > td,\n.table-hover .table-light:hover > th {\n background-color: #ececf6;\n}\n\n.table-dark,\n.table-dark > th,\n.table-dark > td {\n background-color: #c6c8ca;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th,\n.table-dark tbody + tbody {\n border-color: #95999c;\n}\n\n.table-hover .table-dark:hover {\n background-color: #b9bbbe;\n}\n\n.table-hover .table-dark:hover > td,\n.table-hover .table-dark:hover > th {\n background-color: #b9bbbe;\n}\n\n.table-active,\n.table-active > th,\n.table-active > td {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table-hover .table-active:hover > td,\n.table-hover .table-active:hover > th {\n background-color: rgba(0, 0, 0, 0.075);\n}\n\n.table .thead-dark th {\n color: #fff;\n background-color: #343a40;\n border-color: #454d55;\n}\n\n.table .thead-light th {\n color: #495057;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.table-dark {\n color: #fff;\n background-color: #343a40;\n}\n\n.table-dark th,\n.table-dark td,\n.table-dark thead th {\n border-color: #454d55;\n}\n\n.table-dark.table-bordered {\n border: 0;\n}\n\n.table-dark.table-striped tbody tr:nth-of-type(odd) {\n background-color: rgba(255, 255, 255, 0.05);\n}\n\n.table-dark.table-hover tbody tr:hover {\n color: #fff;\n background-color: rgba(255, 255, 255, 0.075);\n}\n\n@media (max-width: 575.98px) {\n .table-responsive-sm {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-sm > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 767.98px) {\n .table-responsive-md {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-md > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 991.98px) {\n .table-responsive-lg {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-lg > .table-bordered {\n border: 0;\n }\n}\n\n@media (max-width: 1199.98px) {\n .table-responsive-xl {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n }\n .table-responsive-xl > .table-bordered {\n border: 0;\n }\n}\n\n.table-responsive {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.table-responsive > .table-bordered {\n border: 0;\n}\n\n.form-control {\n display: block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .form-control {\n transition: none;\n }\n}\n\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n\n.form-control:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.form-control:focus {\n color: #495057;\n background-color: #fff;\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.form-control::placeholder {\n color: #6c757d;\n opacity: 1;\n}\n\n.form-control:disabled, .form-control[readonly] {\n background-color: #e9ecef;\n opacity: 1;\n}\n\ninput[type=\"date\"].form-control,\ninput[type=\"time\"].form-control,\ninput[type=\"datetime-local\"].form-control,\ninput[type=\"month\"].form-control {\n appearance: none;\n}\n\nselect.form-control:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n.col-form-label {\n padding-top: calc(0.375rem + 1px);\n padding-bottom: calc(0.375rem + 1px);\n margin-bottom: 0;\n font-size: inherit;\n line-height: 1.5;\n}\n\n.col-form-label-lg {\n padding-top: calc(0.5rem + 1px);\n padding-bottom: calc(0.5rem + 1px);\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.col-form-label-sm {\n padding-top: calc(0.25rem + 1px);\n padding-bottom: calc(0.25rem + 1px);\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: 0.375rem 0;\n margin-bottom: 0;\n font-size: 1rem;\n line-height: 1.5;\n color: #212529;\n background-color: transparent;\n border: solid transparent;\n border-width: 1px 0;\n}\n\n.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n}\n\n.form-control-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.form-control-lg {\n height: calc(1.5em + 1rem + 2px);\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\nselect.form-control[size], select.form-control[multiple] {\n height: auto;\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n.form-group {\n margin-bottom: 1rem;\n}\n\n.form-text {\n display: block;\n margin-top: 0.25rem;\n}\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -5px;\n margin-left: -5px;\n}\n\n.form-row > .col,\n.form-row > [class*=\"col-\"] {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.form-check {\n position: relative;\n display: block;\n padding-left: 1.25rem;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: 0.3rem;\n margin-left: -1.25rem;\n}\n\n.form-check-input[disabled] ~ .form-check-label,\n.form-check-input:disabled ~ .form-check-label {\n color: #6c757d;\n}\n\n.form-check-label {\n margin-bottom: 0;\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0;\n margin-right: 0.75rem;\n}\n\n.form-check-inline .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: 0.3125rem;\n margin-left: 0;\n}\n\n.valid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #28a745;\n}\n\n.valid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(40, 167, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :valid ~ .valid-feedback,\n.was-validated :valid ~ .valid-tooltip,\n.is-valid ~ .valid-feedback,\n.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:valid, .form-control.is-valid {\n border-color: #28a745;\n padding-right: calc(1.5em + 0.75rem);\n 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='%2328a745' 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\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:valid:focus, .form-control.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated textarea.form-control:valid, textarea.form-control.is-valid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:valid, .custom-select.is-valid {\n border-color: #28a745;\n padding-right: calc(0.75em + 2.3125rem);\n 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='%2328a745' 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\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:valid:focus, .custom-select.is-valid:focus {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {\n color: #28a745;\n}\n\n.was-validated .form-check-input:valid ~ .valid-feedback,\n.was-validated .form-check-input:valid ~ .valid-tooltip, .form-check-input.is-valid ~ .valid-feedback,\n.form-check-input.is-valid ~ .valid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label, .custom-control-input.is-valid ~ .custom-control-label {\n color: #28a745;\n}\n\n.was-validated .custom-control-input:valid ~ .custom-control-label::before, .custom-control-input.is-valid ~ .custom-control-label::before {\n border-color: #28a745;\n}\n\n.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before, .custom-control-input.is-valid:checked ~ .custom-control-label::before {\n border-color: #34ce57;\n background-color: #34ce57;\n}\n\n.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before, .custom-control-input.is-valid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.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 {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid ~ .custom-file-label, .custom-file-input.is-valid ~ .custom-file-label {\n border-color: #28a745;\n}\n\n.was-validated .custom-file-input:valid:focus ~ .custom-file-label, .custom-file-input.is-valid:focus ~ .custom-file-label {\n border-color: #28a745;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);\n}\n\n.invalid-feedback {\n display: none;\n width: 100%;\n margin-top: 0.25rem;\n font-size: 80%;\n color: #dc3545;\n}\n\n.invalid-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%;\n padding: 0.25rem 0.5rem;\n margin-top: .1rem;\n font-size: 0.875rem;\n line-height: 1.5;\n color: #fff;\n background-color: rgba(220, 53, 69, 0.9);\n border-radius: 0.25rem;\n}\n\n.was-validated :invalid ~ .invalid-feedback,\n.was-validated :invalid ~ .invalid-tooltip,\n.is-invalid ~ .invalid-feedback,\n.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .form-control:invalid, .form-control.is-invalid {\n border-color: #dc3545;\n padding-right: calc(1.5em + 0.75rem);\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' 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='%23dc3545' stroke='none'/%3e%3c/svg%3e\");\n background-repeat: no-repeat;\n background-position: right calc(0.375em + 0.1875rem) center;\n background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {\n padding-right: calc(1.5em + 0.75rem);\n background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);\n}\n\n.was-validated .custom-select:invalid, .custom-select.is-invalid {\n border-color: #dc3545;\n padding-right: calc(0.75em + 2.3125rem);\n 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='%23dc3545' 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='%23dc3545' stroke='none'/%3e%3c/svg%3e\") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);\n}\n\n.was-validated .custom-select:invalid:focus, .custom-select.is-invalid:focus {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {\n color: #dc3545;\n}\n\n.was-validated .form-check-input:invalid ~ .invalid-feedback,\n.was-validated .form-check-input:invalid ~ .invalid-tooltip, .form-check-input.is-invalid ~ .invalid-feedback,\n.form-check-input.is-invalid ~ .invalid-tooltip {\n display: block;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label, .custom-control-input.is-invalid ~ .custom-control-label {\n color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid ~ .custom-control-label::before, .custom-control-input.is-invalid ~ .custom-control-label::before {\n border-color: #dc3545;\n}\n\n.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before, .custom-control-input.is-invalid:checked ~ .custom-control-label::before {\n border-color: #e4606d;\n background-color: #e4606d;\n}\n\n.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before, .custom-control-input.is-invalid:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.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 {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid ~ .custom-file-label, .custom-file-input.is-invalid ~ .custom-file-label {\n border-color: #dc3545;\n}\n\n.was-validated .custom-file-input:invalid:focus ~ .custom-file-label, .custom-file-input.is-invalid:focus ~ .custom-file-label {\n border-color: #dc3545;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);\n}\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center;\n}\n\n.form-inline .form-check {\n width: 100%;\n}\n\n@media (min-width: 576px) {\n .form-inline label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n .form-inline .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-plaintext {\n display: inline-block;\n }\n .form-inline .input-group,\n .form-inline .custom-select {\n width: auto;\n }\n .form-inline .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-inline .form-check-input {\n position: relative;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: 0.25rem;\n margin-left: 0;\n }\n .form-inline .custom-control {\n align-items: center;\n justify-content: center;\n }\n .form-inline .custom-control-label {\n margin-bottom: 0;\n }\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #212529;\n text-align: center;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n padding: 0.375rem 0.75rem;\n font-size: 1rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n 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;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .btn {\n transition: none;\n }\n}\n\n.btn:hover {\n color: #212529;\n text-decoration: none;\n}\n\n.btn:focus, .btn.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.btn.disabled, .btn:disabled {\n opacity: 0.65;\n}\n\n.btn:not(:disabled):not(.disabled) {\n cursor: pointer;\n}\n\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n.btn-primary {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:hover {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n}\n\n.btn-primary:focus, .btn-primary.focus {\n color: #fff;\n background-color: #0069d9;\n border-color: #0062cc;\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-primary.disabled, .btn-primary:disabled {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active, .btn-primary:not(:disabled):not(.disabled).active,\n.show > .btn-primary.dropdown-toggle {\n color: #fff;\n background-color: #0062cc;\n border-color: #005cbf;\n}\n\n.btn-primary:not(:disabled):not(.disabled):active:focus, .btn-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(38, 143, 255, 0.5);\n}\n\n.btn-secondary {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:hover {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n}\n\n.btn-secondary:focus, .btn-secondary.focus {\n color: #fff;\n background-color: #5a6268;\n border-color: #545b62;\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-secondary.disabled, .btn-secondary:disabled {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active, .btn-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-secondary.dropdown-toggle {\n color: #fff;\n background-color: #545b62;\n border-color: #4e555b;\n}\n\n.btn-secondary:not(:disabled):not(.disabled):active:focus, .btn-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);\n}\n\n.btn-success {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:hover {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n}\n\n.btn-success:focus, .btn-success.focus {\n color: #fff;\n background-color: #218838;\n border-color: #1e7e34;\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-success.disabled, .btn-success:disabled {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active,\n.show > .btn-success.dropdown-toggle {\n color: #fff;\n background-color: #1e7e34;\n border-color: #1c7430;\n}\n\n.btn-success:not(:disabled):not(.disabled):active:focus, .btn-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);\n}\n\n.btn-info {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:hover {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n}\n\n.btn-info:focus, .btn-info.focus {\n color: #fff;\n background-color: #138496;\n border-color: #117a8b;\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-info.disabled, .btn-info:disabled {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-info:not(:disabled):not(.disabled):active, .btn-info:not(:disabled):not(.disabled).active,\n.show > .btn-info.dropdown-toggle {\n color: #fff;\n background-color: #117a8b;\n border-color: #10707f;\n}\n\n.btn-info:not(:disabled):not(.disabled):active:focus, .btn-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);\n}\n\n.btn-warning {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:hover {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n}\n\n.btn-warning:focus, .btn-warning.focus {\n color: #212529;\n background-color: #e0a800;\n border-color: #d39e00;\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-warning.disabled, .btn-warning:disabled {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active, .btn-warning:not(:disabled):not(.disabled).active,\n.show > .btn-warning.dropdown-toggle {\n color: #212529;\n background-color: #d39e00;\n border-color: #c69500;\n}\n\n.btn-warning:not(:disabled):not(.disabled):active:focus, .btn-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);\n}\n\n.btn-danger {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:hover {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n}\n\n.btn-danger:focus, .btn-danger.focus {\n color: #fff;\n background-color: #c82333;\n border-color: #bd2130;\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-danger.disabled, .btn-danger:disabled {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active, .btn-danger:not(:disabled):not(.disabled).active,\n.show > .btn-danger.dropdown-toggle {\n color: #fff;\n background-color: #bd2130;\n border-color: #b21f2d;\n}\n\n.btn-danger:not(:disabled):not(.disabled):active:focus, .btn-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);\n}\n\n.btn-light {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:hover {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n}\n\n.btn-light:focus, .btn-light.focus {\n color: #212529;\n background-color: #e2e6ea;\n border-color: #dae0e5;\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-light.disabled, .btn-light:disabled {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-light:not(:disabled):not(.disabled):active, .btn-light:not(:disabled):not(.disabled).active,\n.show > .btn-light.dropdown-toggle {\n color: #212529;\n background-color: #dae0e5;\n border-color: #d3d9df;\n}\n\n.btn-light:not(:disabled):not(.disabled):active:focus, .btn-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);\n}\n\n.btn-dark {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:hover {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n}\n\n.btn-dark:focus, .btn-dark.focus {\n color: #fff;\n background-color: #23272b;\n border-color: #1d2124;\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-dark.disabled, .btn-dark:disabled {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active, .btn-dark:not(:disabled):not(.disabled).active,\n.show > .btn-dark.dropdown-toggle {\n color: #fff;\n background-color: #1d2124;\n border-color: #171a1d;\n}\n\n.btn-dark:not(:disabled):not(.disabled):active:focus, .btn-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);\n}\n\n.btn-outline-primary {\n color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:hover {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:focus, .btn-outline-primary.focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-primary.disabled, .btn-outline-primary:disabled {\n color: #007bff;\n background-color: transparent;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-primary.dropdown-toggle {\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.btn-outline-primary:not(:disabled):not(.disabled):active:focus, .btn-outline-primary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-primary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.btn-outline-secondary {\n color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:hover {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:focus, .btn-outline-secondary.focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-secondary.disabled, .btn-outline-secondary:disabled {\n color: #6c757d;\n background-color: transparent;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active, .btn-outline-secondary:not(:disabled):not(.disabled).active,\n.show > .btn-outline-secondary.dropdown-toggle {\n color: #fff;\n background-color: #6c757d;\n border-color: #6c757d;\n}\n\n.btn-outline-secondary:not(:disabled):not(.disabled):active:focus, .btn-outline-secondary:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-secondary.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.btn-outline-success {\n color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:hover {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:focus, .btn-outline-success.focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-success.disabled, .btn-outline-success:disabled {\n color: #28a745;\n background-color: transparent;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active, .btn-outline-success:not(:disabled):not(.disabled).active,\n.show > .btn-outline-success.dropdown-toggle {\n color: #fff;\n background-color: #28a745;\n border-color: #28a745;\n}\n\n.btn-outline-success:not(:disabled):not(.disabled):active:focus, .btn-outline-success:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-success.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.btn-outline-info {\n color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:hover {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:focus, .btn-outline-info.focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-info.disabled, .btn-outline-info:disabled {\n color: #17a2b8;\n background-color: transparent;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active, .btn-outline-info:not(:disabled):not(.disabled).active,\n.show > .btn-outline-info.dropdown-toggle {\n color: #fff;\n background-color: #17a2b8;\n border-color: #17a2b8;\n}\n\n.btn-outline-info:not(:disabled):not(.disabled):active:focus, .btn-outline-info:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-info.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.btn-outline-warning {\n color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:hover {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:focus, .btn-outline-warning.focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-warning.disabled, .btn-outline-warning:disabled {\n color: #ffc107;\n background-color: transparent;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active, .btn-outline-warning:not(:disabled):not(.disabled).active,\n.show > .btn-outline-warning.dropdown-toggle {\n color: #212529;\n background-color: #ffc107;\n border-color: #ffc107;\n}\n\n.btn-outline-warning:not(:disabled):not(.disabled):active:focus, .btn-outline-warning:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-warning.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.btn-outline-danger {\n color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:hover {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:focus, .btn-outline-danger.focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-danger.disabled, .btn-outline-danger:disabled {\n color: #dc3545;\n background-color: transparent;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active, .btn-outline-danger:not(:disabled):not(.disabled).active,\n.show > .btn-outline-danger.dropdown-toggle {\n color: #fff;\n background-color: #dc3545;\n border-color: #dc3545;\n}\n\n.btn-outline-danger:not(:disabled):not(.disabled):active:focus, .btn-outline-danger:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-danger.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.btn-outline-light {\n color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:hover {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:focus, .btn-outline-light.focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-light.disabled, .btn-outline-light:disabled {\n color: #f8f9fa;\n background-color: transparent;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active, .btn-outline-light:not(:disabled):not(.disabled).active,\n.show > .btn-outline-light.dropdown-toggle {\n color: #212529;\n background-color: #f8f9fa;\n border-color: #f8f9fa;\n}\n\n.btn-outline-light:not(:disabled):not(.disabled):active:focus, .btn-outline-light:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-light.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.btn-outline-dark {\n color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:hover {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:focus, .btn-outline-dark.focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-outline-dark.disabled, .btn-outline-dark:disabled {\n color: #343a40;\n background-color: transparent;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active, .btn-outline-dark:not(:disabled):not(.disabled).active,\n.show > .btn-outline-dark.dropdown-toggle {\n color: #fff;\n background-color: #343a40;\n border-color: #343a40;\n}\n\n.btn-outline-dark:not(:disabled):not(.disabled):active:focus, .btn-outline-dark:not(:disabled):not(.disabled).active:focus,\n.show > .btn-outline-dark.dropdown-toggle:focus {\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.btn-link {\n font-weight: 400;\n color: #007bff;\n text-decoration: none;\n}\n\n.btn-link:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\n.btn-link:focus, .btn-link.focus {\n text-decoration: underline;\n}\n\n.btn-link:disabled, .btn-link.disabled {\n color: #6c757d;\n pointer-events: none;\n}\n\n.btn-lg, .btn-group-lg > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.btn-sm, .btn-group-sm > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.btn-block {\n display: block;\n width: 100%;\n}\n\n.btn-block + .btn-block {\n margin-top: 0.5rem;\n}\n\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n\n.fade {\n transition: opacity 0.15s linear;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .fade {\n transition: none;\n }\n}\n\n.fade:not(.show) {\n opacity: 0;\n}\n\n.collapse:not(.show) {\n display: none;\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n transition: height 0.35s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .collapsing {\n transition: none;\n }\n}\n\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n}\n\n.dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-bottom: 0;\n border-left: 0.3em solid transparent;\n}\n\n.dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 10rem;\n padding: 0.5rem 0;\n margin: 0.125rem 0 0;\n font-size: 1rem;\n color: #212529;\n text-align: left;\n list-style: none;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 0.25rem;\n}\n\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n\n@media (min-width: 576px) {\n .dropdown-menu-sm-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-sm-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 768px) {\n .dropdown-menu-md-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-md-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 992px) {\n .dropdown-menu-lg-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-lg-right {\n right: 0;\n left: auto;\n }\n}\n\n@media (min-width: 1200px) {\n .dropdown-menu-xl-left {\n right: auto;\n left: 0;\n }\n .dropdown-menu-xl-right {\n right: 0;\n left: auto;\n }\n}\n\n.dropup .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 0.125rem;\n}\n\n.dropup .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0;\n border-right: 0.3em solid transparent;\n border-bottom: 0.3em solid;\n border-left: 0.3em solid transparent;\n}\n\n.dropup .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: 0.125rem;\n}\n\n.dropright .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0;\n border-bottom: 0.3em solid transparent;\n border-left: 0.3em solid;\n}\n\n.dropright .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropright .dropdown-toggle::after {\n vertical-align: 0;\n}\n\n.dropleft .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: 0.125rem;\n}\n\n.dropleft .dropdown-toggle::after {\n display: inline-block;\n margin-left: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n}\n\n.dropleft .dropdown-toggle::after {\n display: none;\n}\n\n.dropleft .dropdown-toggle::before {\n display: inline-block;\n margin-right: 0.255em;\n vertical-align: 0.255em;\n content: \"\";\n border-top: 0.3em solid transparent;\n border-right: 0.3em solid;\n border-bottom: 0.3em solid transparent;\n}\n\n.dropleft .dropdown-toggle:empty::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle::before {\n vertical-align: 0;\n}\n\n.dropdown-menu[x-placement^=\"top\"], .dropdown-menu[x-placement^=\"right\"], .dropdown-menu[x-placement^=\"bottom\"], .dropdown-menu[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n}\n\n.dropdown-divider {\n height: 0;\n margin: 0.5rem 0;\n overflow: hidden;\n border-top: 1px solid #e9ecef;\n}\n\n.dropdown-item {\n display: block;\n width: 100%;\n padding: 0.25rem 1.5rem;\n clear: both;\n font-weight: 400;\n color: #212529;\n text-align: inherit;\n white-space: nowrap;\n background-color: transparent;\n border: 0;\n}\n\n.dropdown-item:hover, .dropdown-item:focus {\n color: #16181b;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.dropdown-item.active, .dropdown-item:active {\n color: #fff;\n text-decoration: none;\n background-color: #007bff;\n}\n\n.dropdown-item.disabled, .dropdown-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: transparent;\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n.dropdown-header {\n display: block;\n padding: 0.5rem 1.5rem;\n margin-bottom: 0;\n font-size: 0.875rem;\n color: #6c757d;\n white-space: nowrap;\n}\n\n.dropdown-item-text {\n display: block;\n padding: 0.25rem 1.5rem;\n color: #212529;\n}\n\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle;\n}\n\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n flex: 1 1 auto;\n}\n\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover {\n z-index: 1;\n}\n\n.btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active,\n.btn-group-vertical > .btn:focus,\n.btn-group-vertical > .btn:active,\n.btn-group-vertical > .btn.active {\n z-index: 1;\n}\n\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n}\n\n.btn-toolbar .input-group {\n width: auto;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) {\n margin-left: -1px;\n}\n\n.btn-group > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.btn-group > .btn:not(:first-child),\n.btn-group > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.dropdown-toggle-split {\n padding-right: 0.5625rem;\n padding-left: 0.5625rem;\n}\n\n.dropdown-toggle-split::after,\n.dropup .dropdown-toggle-split::after,\n.dropright .dropdown-toggle-split::after {\n margin-left: 0;\n}\n\n.dropleft .dropdown-toggle-split::before {\n margin-right: 0;\n}\n\n.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {\n padding-right: 0.375rem;\n padding-left: 0.375rem;\n}\n\n.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {\n padding-right: 0.75rem;\n padding-left: 0.75rem;\n}\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n}\n\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group {\n width: 100%;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) {\n margin-top: -1px;\n}\n\n.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),\n.btn-group-vertical > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.btn-group-vertical > .btn:not(:first-child),\n.btn-group-vertical > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.btn-group-toggle > .btn,\n.btn-group-toggle > .btn-group > .btn {\n margin-bottom: 0;\n}\n\n.btn-group-toggle > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn input[type=\"checkbox\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"radio\"],\n.btn-group-toggle > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n width: 100%;\n}\n\n.input-group > .form-control,\n.input-group > .form-control-plaintext,\n.input-group > .custom-select,\n.input-group > .custom-file {\n position: relative;\n flex: 1 1 auto;\n width: 1%;\n min-width: 0;\n margin-bottom: 0;\n}\n\n.input-group > .form-control + .form-control,\n.input-group > .form-control + .custom-select,\n.input-group > .form-control + .custom-file,\n.input-group > .form-control-plaintext + .form-control,\n.input-group > .form-control-plaintext + .custom-select,\n.input-group > .form-control-plaintext + .custom-file,\n.input-group > .custom-select + .form-control,\n.input-group > .custom-select + .custom-select,\n.input-group > .custom-select + .custom-file,\n.input-group > .custom-file + .form-control,\n.input-group > .custom-file + .custom-select,\n.input-group > .custom-file + .custom-file {\n margin-left: -1px;\n}\n\n.input-group > .form-control:focus,\n.input-group > .custom-select:focus,\n.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n}\n\n.input-group > .custom-file .custom-file-input:focus {\n z-index: 4;\n}\n\n.input-group > .form-control:not(:last-child),\n.input-group > .custom-select:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .form-control:not(:first-child),\n.input-group > .custom-select:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group > .custom-file {\n display: flex;\n align-items: center;\n}\n\n.input-group > .custom-file:not(:last-child) .custom-file-label,\n.input-group > .custom-file:not(:last-child) .custom-file-label::after {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .custom-file:not(:first-child) .custom-file-label {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n}\n\n.input-group-prepend .btn,\n.input-group-append .btn {\n position: relative;\n z-index: 2;\n}\n\n.input-group-prepend .btn:focus,\n.input-group-append .btn:focus {\n z-index: 3;\n}\n\n.input-group-prepend .btn + .btn,\n.input-group-prepend .btn + .input-group-text,\n.input-group-prepend .input-group-text + .input-group-text,\n.input-group-prepend .input-group-text + .btn,\n.input-group-append .btn + .btn,\n.input-group-append .btn + .input-group-text,\n.input-group-append .input-group-text + .input-group-text,\n.input-group-append .input-group-text + .btn {\n margin-left: -1px;\n}\n\n.input-group-prepend {\n margin-right: -1px;\n}\n\n.input-group-append {\n margin-left: -1px;\n}\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: 0.375rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n text-align: center;\n white-space: nowrap;\n background-color: #e9ecef;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.input-group-text input[type=\"radio\"],\n.input-group-text input[type=\"checkbox\"] {\n margin-top: 0;\n}\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: calc(1.5em + 1rem + 2px);\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: 0.5rem 1rem;\n font-size: 1.25rem;\n line-height: 1.5;\n border-radius: 0.3rem;\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: calc(1.5em + 0.5rem + 2px);\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n border-radius: 0.2rem;\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: 1.75rem;\n}\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.custom-control {\n position: relative;\n display: block;\n min-height: 1.5rem;\n padding-left: 1.5rem;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: 1rem;\n}\n\n.custom-control-input {\n position: absolute;\n left: 0;\n z-index: -1;\n width: 1rem;\n height: 1.25rem;\n opacity: 0;\n}\n\n.custom-control-input:checked ~ .custom-control-label::before {\n color: #fff;\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-control-input:focus ~ .custom-control-label::before {\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {\n border-color: #80bdff;\n}\n\n.custom-control-input:not(:disabled):active ~ .custom-control-label::before {\n color: #fff;\n background-color: #b3d7ff;\n border-color: #b3d7ff;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {\n color: #6c757d;\n}\n\n.custom-control-input[disabled] ~ .custom-control-label::before, .custom-control-input:disabled ~ .custom-control-label::before {\n background-color: #e9ecef;\n}\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n vertical-align: top;\n}\n\n.custom-control-label::before {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n pointer-events: none;\n content: \"\";\n background-color: #fff;\n border: #adb5bd solid 1px;\n}\n\n.custom-control-label::after {\n position: absolute;\n top: 0.25rem;\n left: -1.5rem;\n display: block;\n width: 1rem;\n height: 1rem;\n content: \"\";\n background: no-repeat 50% / 50% 50%;\n}\n\n.custom-checkbox .custom-control-label::before {\n border-radius: 0.25rem;\n}\n\n.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {\n 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='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {\n border-color: #007bff;\n background-color: #007bff;\n}\n\n.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {\n 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='%23fff' d='M0 2h4'/%3e%3c/svg%3e\");\n}\n\n.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-radio .custom-control-label::before {\n border-radius: 50%;\n}\n\n.custom-radio .custom-control-input:checked ~ .custom-control-label::after {\n 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='%23fff'/%3e%3c/svg%3e\");\n}\n\n.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-switch {\n padding-left: 2.25rem;\n}\n\n.custom-switch .custom-control-label::before {\n left: -2.25rem;\n width: 1.75rem;\n pointer-events: all;\n border-radius: 0.5rem;\n}\n\n.custom-switch .custom-control-label::after {\n top: calc(0.25rem + 2px);\n left: calc(-2.25rem + 2px);\n width: calc(1rem - 4px);\n height: calc(1rem - 4px);\n background-color: #adb5bd;\n border-radius: 0.5rem;\n 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;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-switch .custom-control-label::after {\n transition: none;\n }\n}\n\n.custom-switch .custom-control-input:checked ~ .custom-control-label::after {\n background-color: #fff;\n transform: translateX(0.75rem);\n}\n\n.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {\n background-color: rgba(0, 123, 255, 0.5);\n}\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 1.75rem 0.375rem 0.75rem;\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n vertical-align: middle;\n background: #fff 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;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n appearance: none;\n}\n\n.custom-select:focus {\n border-color: #80bdff;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-select:focus::-ms-value {\n color: #495057;\n background-color: #fff;\n}\n\n.custom-select[multiple], .custom-select[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: 0.75rem;\n background-image: none;\n}\n\n.custom-select:disabled {\n color: #6c757d;\n background-color: #e9ecef;\n}\n\n.custom-select::-ms-expand {\n display: none;\n}\n\n.custom-select:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 #495057;\n}\n\n.custom-select-sm {\n height: calc(1.5em + 0.5rem + 2px);\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-left: 0.5rem;\n font-size: 0.875rem;\n}\n\n.custom-select-lg {\n height: calc(1.5em + 1rem + 2px);\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n padding-left: 1rem;\n font-size: 1.25rem;\n}\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: calc(1.5em + 0.75rem + 2px);\n margin: 0;\n opacity: 0;\n}\n\n.custom-file-input:focus ~ .custom-file-label {\n border-color: #80bdff;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-file-input[disabled] ~ .custom-file-label,\n.custom-file-input:disabled ~ .custom-file-label {\n background-color: #e9ecef;\n}\n\n.custom-file-input:lang(en) ~ .custom-file-label::after {\n content: \"Browse\";\n}\n\n.custom-file-input ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: calc(1.5em + 0.75rem + 2px);\n padding: 0.375rem 0.75rem;\n font-weight: 400;\n line-height: 1.5;\n color: #495057;\n background-color: #fff;\n border: 1px solid #ced4da;\n border-radius: 0.25rem;\n}\n\n.custom-file-label::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: calc(1.5em + 0.75rem);\n padding: 0.375rem 0.75rem;\n line-height: 1.5;\n color: #495057;\n content: \"Browse\";\n background-color: #e9ecef;\n border-left: inherit;\n border-radius: 0 0.25rem 0.25rem 0;\n}\n\n.custom-range {\n width: 100%;\n height: 1.4rem;\n padding: 0;\n background-color: transparent;\n appearance: none;\n}\n\n.custom-range:focus {\n outline: none;\n}\n\n.custom-range:focus::-webkit-slider-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-moz-range-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range:focus::-ms-thumb {\n box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.custom-range::-moz-focus-outer {\n border: 0;\n}\n\n.custom-range::-webkit-slider-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: -0.25rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-webkit-slider-thumb {\n transition: none;\n }\n}\n\n.custom-range::-webkit-slider-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-webkit-slider-runnable-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-moz-range-thumb {\n width: 1rem;\n height: 1rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-moz-range-thumb {\n transition: none;\n }\n}\n\n.custom-range::-moz-range-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-moz-range-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: #dee2e6;\n border-color: transparent;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-thumb {\n width: 1rem;\n height: 1rem;\n margin-top: 0;\n margin-right: 0.2rem;\n margin-left: 0.2rem;\n background-color: #007bff;\n border: 0;\n border-radius: 1rem;\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n appearance: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-range::-ms-thumb {\n transition: none;\n }\n}\n\n.custom-range::-ms-thumb:active {\n background-color: #b3d7ff;\n}\n\n.custom-range::-ms-track {\n width: 100%;\n height: 0.5rem;\n color: transparent;\n cursor: pointer;\n background-color: transparent;\n border-color: transparent;\n border-width: 0.5rem;\n}\n\n.custom-range::-ms-fill-lower {\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range::-ms-fill-upper {\n margin-right: 15px;\n background-color: #dee2e6;\n border-radius: 1rem;\n}\n\n.custom-range:disabled::-webkit-slider-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-webkit-slider-runnable-track {\n cursor: default;\n}\n\n.custom-range:disabled::-moz-range-thumb {\n background-color: #adb5bd;\n}\n\n.custom-range:disabled::-moz-range-track {\n cursor: default;\n}\n\n.custom-range:disabled::-ms-thumb {\n background-color: #adb5bd;\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .custom-control-label::before,\n .custom-file-label,\n .custom-select {\n transition: none;\n }\n}\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: 0.5rem 1rem;\n}\n\n.nav-link:hover, .nav-link:focus {\n text-decoration: none;\n}\n\n.nav-link.disabled {\n color: #6c757d;\n pointer-events: none;\n cursor: default;\n}\n\n.nav-tabs {\n border-bottom: 1px solid #dee2e6;\n}\n\n.nav-tabs .nav-item {\n margin-bottom: -1px;\n}\n\n.nav-tabs .nav-link {\n border: 1px solid transparent;\n border-top-left-radius: 0.25rem;\n border-top-right-radius: 0.25rem;\n}\n\n.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {\n border-color: #e9ecef #e9ecef #dee2e6;\n}\n\n.nav-tabs .nav-link.disabled {\n color: #6c757d;\n background-color: transparent;\n border-color: transparent;\n}\n\n.nav-tabs .nav-link.active,\n.nav-tabs .nav-item.show .nav-link {\n color: #495057;\n background-color: #fff;\n border-color: #dee2e6 #dee2e6 #fff;\n}\n\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.nav-pills .nav-link {\n border-radius: 0.25rem;\n}\n\n.nav-pills .nav-link.active,\n.nav-pills .show > .nav-link {\n color: #fff;\n background-color: #007bff;\n}\n\n.nav-fill .nav-item {\n flex: 1 1 auto;\n text-align: center;\n}\n\n.nav-justified .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n}\n\n.tab-content > .tab-pane {\n display: none;\n}\n\n.tab-content > .active {\n display: block;\n}\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 1rem;\n}\n\n.navbar .container,\n.navbar .container-fluid, .navbar .container-sm, .navbar .container-md, .navbar .container-lg, .navbar .container-xl {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n}\n\n.navbar-brand {\n display: inline-block;\n padding-top: 0.3125rem;\n padding-bottom: 0.3125rem;\n margin-right: 1rem;\n font-size: 1.25rem;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.navbar-brand:hover, .navbar-brand:focus {\n text-decoration: none;\n}\n\n.navbar-nav {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.navbar-nav .nav-link {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-nav .dropdown-menu {\n position: static;\n float: none;\n}\n\n.navbar-text {\n display: inline-block;\n padding-top: 0.5rem;\n padding-bottom: 0.5rem;\n}\n\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n align-items: center;\n}\n\n.navbar-toggler {\n padding: 0.25rem 0.75rem;\n font-size: 1.25rem;\n line-height: 1;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.navbar-toggler:hover, .navbar-toggler:focus {\n text-decoration: none;\n}\n\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n@media (max-width: 575.98px) {\n .navbar-expand-sm > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 576px) {\n .navbar-expand-sm {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-sm .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-sm .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-sm .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-sm > .container,\n .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 {\n flex-wrap: nowrap;\n }\n .navbar-expand-sm .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-sm .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 767.98px) {\n .navbar-expand-md > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 768px) {\n .navbar-expand-md {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-md .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-md .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-md .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-md > .container,\n .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 {\n flex-wrap: nowrap;\n }\n .navbar-expand-md .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-md .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 991.98px) {\n .navbar-expand-lg > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 992px) {\n .navbar-expand-lg {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-lg .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-lg .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-lg .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-lg > .container,\n .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 {\n flex-wrap: nowrap;\n }\n .navbar-expand-lg .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-lg .navbar-toggler {\n display: none;\n }\n}\n\n@media (max-width: 1199.98px) {\n .navbar-expand-xl > .container,\n .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 {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n@media (min-width: 1200px) {\n .navbar-expand-xl {\n flex-flow: row nowrap;\n justify-content: flex-start;\n }\n .navbar-expand-xl .navbar-nav {\n flex-direction: row;\n }\n .navbar-expand-xl .navbar-nav .dropdown-menu {\n position: absolute;\n }\n .navbar-expand-xl .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n }\n .navbar-expand-xl > .container,\n .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 {\n flex-wrap: nowrap;\n }\n .navbar-expand-xl .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n }\n .navbar-expand-xl .navbar-toggler {\n display: none;\n }\n}\n\n.navbar-expand {\n flex-flow: row nowrap;\n justify-content: flex-start;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n padding-right: 0;\n padding-left: 0;\n}\n\n.navbar-expand .navbar-nav {\n flex-direction: row;\n}\n\n.navbar-expand .navbar-nav .dropdown-menu {\n position: absolute;\n}\n\n.navbar-expand .navbar-nav .nav-link {\n padding-right: 0.5rem;\n padding-left: 0.5rem;\n}\n\n.navbar-expand > .container,\n.navbar-expand > .container-fluid, .navbar-expand > .container-sm, .navbar-expand > .container-md, .navbar-expand > .container-lg, .navbar-expand > .container-xl {\n flex-wrap: nowrap;\n}\n\n.navbar-expand .navbar-collapse {\n display: flex !important;\n flex-basis: auto;\n}\n\n.navbar-expand .navbar-toggler {\n display: none;\n}\n\n.navbar-light .navbar-brand {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-nav .nav-link {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {\n color: rgba(0, 0, 0, 0.7);\n}\n\n.navbar-light .navbar-nav .nav-link.disabled {\n color: rgba(0, 0, 0, 0.3);\n}\n\n.navbar-light .navbar-nav .show > .nav-link,\n.navbar-light .navbar-nav .active > .nav-link,\n.navbar-light .navbar-nav .nav-link.show,\n.navbar-light .navbar-nav .nav-link.active {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-toggler {\n color: rgba(0, 0, 0, 0.5);\n border-color: rgba(0, 0, 0, 0.1);\n}\n\n.navbar-light .navbar-toggler-icon {\n 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\");\n}\n\n.navbar-light .navbar-text {\n color: rgba(0, 0, 0, 0.5);\n}\n\n.navbar-light .navbar-text a {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-light .navbar-text a:hover, .navbar-light .navbar-text a:focus {\n color: rgba(0, 0, 0, 0.9);\n}\n\n.navbar-dark .navbar-brand {\n color: #fff;\n}\n\n.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {\n color: #fff;\n}\n\n.navbar-dark .navbar-nav .nav-link {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {\n color: rgba(255, 255, 255, 0.75);\n}\n\n.navbar-dark .navbar-nav .nav-link.disabled {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.navbar-dark .navbar-nav .show > .nav-link,\n.navbar-dark .navbar-nav .active > .nav-link,\n.navbar-dark .navbar-nav .nav-link.show,\n.navbar-dark .navbar-nav .nav-link.active {\n color: #fff;\n}\n\n.navbar-dark .navbar-toggler {\n color: rgba(255, 255, 255, 0.5);\n border-color: rgba(255, 255, 255, 0.1);\n}\n\n.navbar-dark .navbar-toggler-icon {\n 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\");\n}\n\n.navbar-dark .navbar-text {\n color: rgba(255, 255, 255, 0.5);\n}\n\n.navbar-dark .navbar-text a {\n color: #fff;\n}\n\n.navbar-dark .navbar-text a:hover, .navbar-dark .navbar-text a:focus {\n color: #fff;\n}\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: border-box;\n border: 1px solid rgba(0, 0, 0, 0.125);\n border-radius: 0.25rem;\n}\n\n.card > hr {\n margin-right: 0;\n margin-left: 0;\n}\n\n.card > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n}\n\n.card > .list-group:first-child {\n border-top-width: 0;\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card > .list-group:last-child {\n border-bottom-width: 0;\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-body {\n flex: 1 1 auto;\n min-height: 1px;\n padding: 1.25rem;\n}\n\n.card-title {\n margin-bottom: 0.75rem;\n}\n\n.card-subtitle {\n margin-top: -0.375rem;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link:hover {\n text-decoration: none;\n}\n\n.card-link + .card-link {\n margin-left: 1.25rem;\n}\n\n.card-header {\n padding: 0.75rem 1.25rem;\n margin-bottom: 0;\n background-color: rgba(0, 0, 0, 0.03);\n border-bottom: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-header:first-child {\n border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;\n}\n\n.card-header + .list-group .list-group-item:first-child {\n border-top: 0;\n}\n\n.card-footer {\n padding: 0.75rem 1.25rem;\n background-color: rgba(0, 0, 0, 0.03);\n border-top: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.card-footer:last-child {\n border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);\n}\n\n.card-header-tabs {\n margin-right: -0.625rem;\n margin-bottom: -0.75rem;\n margin-left: -0.625rem;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -0.625rem;\n margin-left: -0.625rem;\n}\n\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 1.25rem;\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n flex-shrink: 0;\n width: 100%;\n}\n\n.card-img,\n.card-img-top {\n border-top-left-radius: calc(0.25rem - 1px);\n border-top-right-radius: calc(0.25rem - 1px);\n}\n\n.card-img,\n.card-img-bottom {\n border-bottom-right-radius: calc(0.25rem - 1px);\n border-bottom-left-radius: calc(0.25rem - 1px);\n}\n\n.card-deck .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-deck {\n display: flex;\n flex-flow: row wrap;\n margin-right: -15px;\n margin-left: -15px;\n }\n .card-deck .card {\n flex: 1 0 0%;\n margin-right: 15px;\n margin-bottom: 0;\n margin-left: 15px;\n }\n}\n\n.card-group > .card {\n margin-bottom: 15px;\n}\n\n@media (min-width: 576px) {\n .card-group {\n display: flex;\n flex-flow: row wrap;\n }\n .card-group > .card {\n flex: 1 0 0%;\n margin-bottom: 0;\n }\n .card-group > .card + .card {\n margin-left: 0;\n border-left: 0;\n }\n .card-group > .card:not(:last-child) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-top,\n .card-group > .card:not(:last-child) .card-header {\n border-top-right-radius: 0;\n }\n .card-group > .card:not(:last-child) .card-img-bottom,\n .card-group > .card:not(:last-child) .card-footer {\n border-bottom-right-radius: 0;\n }\n .card-group > .card:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-top,\n .card-group > .card:not(:first-child) .card-header {\n border-top-left-radius: 0;\n }\n .card-group > .card:not(:first-child) .card-img-bottom,\n .card-group > .card:not(:first-child) .card-footer {\n border-bottom-left-radius: 0;\n }\n}\n\n.card-columns .card {\n margin-bottom: 0.75rem;\n}\n\n@media (min-width: 576px) {\n .card-columns {\n column-count: 3;\n column-gap: 1.25rem;\n orphans: 1;\n widows: 1;\n }\n .card-columns .card {\n display: inline-block;\n width: 100%;\n }\n}\n\n.accordion > .card {\n overflow: hidden;\n}\n\n.accordion > .card:not(:last-of-type) {\n border-bottom: 0;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.accordion > .card:not(:first-of-type) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n.accordion > .card > .card-header {\n border-radius: 0;\n margin-bottom: -1px;\n}\n\n.breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: 0.75rem 1rem;\n margin-bottom: 1rem;\n list-style: none;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.breadcrumb-item {\n display: flex;\n}\n\n.breadcrumb-item + .breadcrumb-item {\n padding-left: 0.5rem;\n}\n\n.breadcrumb-item + .breadcrumb-item::before {\n display: inline-block;\n padding-right: 0.5rem;\n color: #6c757d;\n content: \"/\";\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: underline;\n}\n\n.breadcrumb-item + .breadcrumb-item:hover::before {\n text-decoration: none;\n}\n\n.breadcrumb-item.active {\n color: #6c757d;\n}\n\n.pagination {\n display: flex;\n padding-left: 0;\n list-style: none;\n border-radius: 0.25rem;\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: 0.5rem 0.75rem;\n margin-left: -1px;\n line-height: 1.25;\n color: #007bff;\n background-color: #fff;\n border: 1px solid #dee2e6;\n}\n\n.page-link:hover {\n z-index: 2;\n color: #0056b3;\n text-decoration: none;\n background-color: #e9ecef;\n border-color: #dee2e6;\n}\n\n.page-link:focus {\n z-index: 3;\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.page-item:first-child .page-link {\n margin-left: 0;\n border-top-left-radius: 0.25rem;\n border-bottom-left-radius: 0.25rem;\n}\n\n.page-item:last-child .page-link {\n border-top-right-radius: 0.25rem;\n border-bottom-right-radius: 0.25rem;\n}\n\n.page-item.active .page-link {\n z-index: 3;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.page-item.disabled .page-link {\n color: #6c757d;\n pointer-events: none;\n cursor: auto;\n background-color: #fff;\n border-color: #dee2e6;\n}\n\n.pagination-lg .page-link {\n padding: 0.75rem 1.5rem;\n font-size: 1.25rem;\n line-height: 1.5;\n}\n\n.pagination-lg .page-item:first-child .page-link {\n border-top-left-radius: 0.3rem;\n border-bottom-left-radius: 0.3rem;\n}\n\n.pagination-lg .page-item:last-child .page-link {\n border-top-right-radius: 0.3rem;\n border-bottom-right-radius: 0.3rem;\n}\n\n.pagination-sm .page-link {\n padding: 0.25rem 0.5rem;\n font-size: 0.875rem;\n line-height: 1.5;\n}\n\n.pagination-sm .page-item:first-child .page-link {\n border-top-left-radius: 0.2rem;\n border-bottom-left-radius: 0.2rem;\n}\n\n.pagination-sm .page-item:last-child .page-link {\n border-top-right-radius: 0.2rem;\n border-bottom-right-radius: 0.2rem;\n}\n\n.badge {\n display: inline-block;\n padding: 0.25em 0.4em;\n font-size: 75%;\n font-weight: 700;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: 0.25rem;\n 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;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .badge {\n transition: none;\n }\n}\n\na.badge:hover, a.badge:focus {\n text-decoration: none;\n}\n\n.badge:empty {\n display: none;\n}\n\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n.badge-pill {\n padding-right: 0.6em;\n padding-left: 0.6em;\n border-radius: 10rem;\n}\n\n.badge-primary {\n color: #fff;\n background-color: #007bff;\n}\n\na.badge-primary:hover, a.badge-primary:focus {\n color: #fff;\n background-color: #0062cc;\n}\n\na.badge-primary:focus, a.badge-primary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.5);\n}\n\n.badge-secondary {\n color: #fff;\n background-color: #6c757d;\n}\n\na.badge-secondary:hover, a.badge-secondary:focus {\n color: #fff;\n background-color: #545b62;\n}\n\na.badge-secondary:focus, a.badge-secondary.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(108, 117, 125, 0.5);\n}\n\n.badge-success {\n color: #fff;\n background-color: #28a745;\n}\n\na.badge-success:hover, a.badge-success:focus {\n color: #fff;\n background-color: #1e7e34;\n}\n\na.badge-success:focus, a.badge-success.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);\n}\n\n.badge-info {\n color: #fff;\n background-color: #17a2b8;\n}\n\na.badge-info:hover, a.badge-info:focus {\n color: #fff;\n background-color: #117a8b;\n}\n\na.badge-info:focus, a.badge-info.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);\n}\n\n.badge-warning {\n color: #212529;\n background-color: #ffc107;\n}\n\na.badge-warning:hover, a.badge-warning:focus {\n color: #212529;\n background-color: #d39e00;\n}\n\na.badge-warning:focus, a.badge-warning.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);\n}\n\n.badge-danger {\n color: #fff;\n background-color: #dc3545;\n}\n\na.badge-danger:hover, a.badge-danger:focus {\n color: #fff;\n background-color: #bd2130;\n}\n\na.badge-danger:focus, a.badge-danger.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);\n}\n\n.badge-light {\n color: #212529;\n background-color: #f8f9fa;\n}\n\na.badge-light:hover, a.badge-light:focus {\n color: #212529;\n background-color: #dae0e5;\n}\n\na.badge-light:focus, a.badge-light.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);\n}\n\n.badge-dark {\n color: #fff;\n background-color: #343a40;\n}\n\na.badge-dark:hover, a.badge-dark:focus {\n color: #fff;\n background-color: #1d2124;\n}\n\na.badge-dark:focus, a.badge-dark.focus {\n outline: 0;\n box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);\n}\n\n.jumbotron {\n padding: 2rem 1rem;\n margin-bottom: 2rem;\n background-color: #e9ecef;\n border-radius: 0.3rem;\n}\n\n@media (min-width: 576px) {\n .jumbotron {\n padding: 4rem 2rem;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n border-radius: 0;\n}\n\n.alert {\n position: relative;\n padding: 0.75rem 1.25rem;\n margin-bottom: 1rem;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n}\n\n.alert-heading {\n color: inherit;\n}\n\n.alert-link {\n font-weight: 700;\n}\n\n.alert-dismissible {\n padding-right: 4rem;\n}\n\n.alert-dismissible .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: 0.75rem 1.25rem;\n color: inherit;\n}\n\n.alert-primary {\n color: #004085;\n background-color: #cce5ff;\n border-color: #b8daff;\n}\n\n.alert-primary hr {\n border-top-color: #9fcdff;\n}\n\n.alert-primary .alert-link {\n color: #002752;\n}\n\n.alert-secondary {\n color: #383d41;\n background-color: #e2e3e5;\n border-color: #d6d8db;\n}\n\n.alert-secondary hr {\n border-top-color: #c8cbcf;\n}\n\n.alert-secondary .alert-link {\n color: #202326;\n}\n\n.alert-success {\n color: #155724;\n background-color: #d4edda;\n border-color: #c3e6cb;\n}\n\n.alert-success hr {\n border-top-color: #b1dfbb;\n}\n\n.alert-success .alert-link {\n color: #0b2e13;\n}\n\n.alert-info {\n color: #0c5460;\n background-color: #d1ecf1;\n border-color: #bee5eb;\n}\n\n.alert-info hr {\n border-top-color: #abdde5;\n}\n\n.alert-info .alert-link {\n color: #062c33;\n}\n\n.alert-warning {\n color: #856404;\n background-color: #fff3cd;\n border-color: #ffeeba;\n}\n\n.alert-warning hr {\n border-top-color: #ffe8a1;\n}\n\n.alert-warning .alert-link {\n color: #533f03;\n}\n\n.alert-danger {\n color: #721c24;\n background-color: #f8d7da;\n border-color: #f5c6cb;\n}\n\n.alert-danger hr {\n border-top-color: #f1b0b7;\n}\n\n.alert-danger .alert-link {\n color: #491217;\n}\n\n.alert-light {\n color: #818182;\n background-color: #fefefe;\n border-color: #fdfdfe;\n}\n\n.alert-light hr {\n border-top-color: #ececf6;\n}\n\n.alert-light .alert-link {\n color: #686868;\n}\n\n.alert-dark {\n color: #1b1e21;\n background-color: #d6d8d9;\n border-color: #c6c8ca;\n}\n\n.alert-dark hr {\n border-top-color: #b9bbbe;\n}\n\n.alert-dark .alert-link {\n color: #040505;\n}\n\n@keyframes progress-bar-stripes {\n from {\n background-position: 1rem 0;\n }\n to {\n background-position: 0 0;\n }\n}\n\n.progress {\n display: flex;\n height: 1rem;\n overflow: hidden;\n line-height: 0;\n font-size: 0.75rem;\n background-color: #e9ecef;\n border-radius: 0.25rem;\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n background-color: #007bff;\n transition: width 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar {\n transition: none;\n }\n}\n\n.progress-bar-striped {\n 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);\n background-size: 1rem 1rem;\n}\n\n.progress-bar-animated {\n animation: progress-bar-stripes 1s linear infinite;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .progress-bar-animated {\n animation: none;\n }\n}\n\n.media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n\n.list-group {\n display: flex;\n flex-direction: column;\n padding-left: 0;\n margin-bottom: 0;\n border-radius: 0.25rem;\n}\n\n.list-group-item-action {\n width: 100%;\n color: #495057;\n text-align: inherit;\n}\n\n.list-group-item-action:hover, .list-group-item-action:focus {\n z-index: 1;\n color: #495057;\n text-decoration: none;\n background-color: #f8f9fa;\n}\n\n.list-group-item-action:active {\n color: #212529;\n background-color: #e9ecef;\n}\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 0.75rem 1.25rem;\n background-color: #fff;\n border: 1px solid rgba(0, 0, 0, 0.125);\n}\n\n.list-group-item:first-child {\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n}\n\n.list-group-item:last-child {\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n}\n\n.list-group-item.disabled, .list-group-item:disabled {\n color: #6c757d;\n pointer-events: none;\n background-color: #fff;\n}\n\n.list-group-item.active {\n z-index: 2;\n color: #fff;\n background-color: #007bff;\n border-color: #007bff;\n}\n\n.list-group-item + .list-group-item {\n border-top-width: 0;\n}\n\n.list-group-item + .list-group-item.active {\n margin-top: -1px;\n border-top-width: 1px;\n}\n\n.list-group-horizontal {\n flex-direction: row;\n}\n\n.list-group-horizontal > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n}\n\n.list-group-horizontal > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n}\n\n.list-group-horizontal > .list-group-item.active {\n margin-top: 0;\n}\n\n.list-group-horizontal > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n}\n\n.list-group-horizontal > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n}\n\n@media (min-width: 576px) {\n .list-group-horizontal-sm {\n flex-direction: row;\n }\n .list-group-horizontal-sm > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-sm > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-sm > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 768px) {\n .list-group-horizontal-md {\n flex-direction: row;\n }\n .list-group-horizontal-md > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-md > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-md > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 992px) {\n .list-group-horizontal-lg {\n flex-direction: row;\n }\n .list-group-horizontal-lg > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-lg > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-lg > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n@media (min-width: 1200px) {\n .list-group-horizontal-xl {\n flex-direction: row;\n }\n .list-group-horizontal-xl > .list-group-item:first-child {\n border-bottom-left-radius: 0.25rem;\n border-top-right-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item:last-child {\n border-top-right-radius: 0.25rem;\n border-bottom-left-radius: 0;\n }\n .list-group-horizontal-xl > .list-group-item.active {\n margin-top: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item {\n border-top-width: 1px;\n border-left-width: 0;\n }\n .list-group-horizontal-xl > .list-group-item + .list-group-item.active {\n margin-left: -1px;\n border-left-width: 1px;\n }\n}\n\n.list-group-flush {\n border-radius: 0;\n}\n\n.list-group-flush > .list-group-item {\n border-width: 0 0 1px;\n}\n\n.list-group-flush > .list-group-item:last-child {\n border-bottom-width: 0;\n}\n\n.list-group-item-primary {\n color: #004085;\n background-color: #b8daff;\n}\n\n.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {\n color: #004085;\n background-color: #9fcdff;\n}\n\n.list-group-item-primary.list-group-item-action.active {\n color: #fff;\n background-color: #004085;\n border-color: #004085;\n}\n\n.list-group-item-secondary {\n color: #383d41;\n background-color: #d6d8db;\n}\n\n.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {\n color: #383d41;\n background-color: #c8cbcf;\n}\n\n.list-group-item-secondary.list-group-item-action.active {\n color: #fff;\n background-color: #383d41;\n border-color: #383d41;\n}\n\n.list-group-item-success {\n color: #155724;\n background-color: #c3e6cb;\n}\n\n.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {\n color: #155724;\n background-color: #b1dfbb;\n}\n\n.list-group-item-success.list-group-item-action.active {\n color: #fff;\n background-color: #155724;\n border-color: #155724;\n}\n\n.list-group-item-info {\n color: #0c5460;\n background-color: #bee5eb;\n}\n\n.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {\n color: #0c5460;\n background-color: #abdde5;\n}\n\n.list-group-item-info.list-group-item-action.active {\n color: #fff;\n background-color: #0c5460;\n border-color: #0c5460;\n}\n\n.list-group-item-warning {\n color: #856404;\n background-color: #ffeeba;\n}\n\n.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {\n color: #856404;\n background-color: #ffe8a1;\n}\n\n.list-group-item-warning.list-group-item-action.active {\n color: #fff;\n background-color: #856404;\n border-color: #856404;\n}\n\n.list-group-item-danger {\n color: #721c24;\n background-color: #f5c6cb;\n}\n\n.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {\n color: #721c24;\n background-color: #f1b0b7;\n}\n\n.list-group-item-danger.list-group-item-action.active {\n color: #fff;\n background-color: #721c24;\n border-color: #721c24;\n}\n\n.list-group-item-light {\n color: #818182;\n background-color: #fdfdfe;\n}\n\n.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {\n color: #818182;\n background-color: #ececf6;\n}\n\n.list-group-item-light.list-group-item-action.active {\n color: #fff;\n background-color: #818182;\n border-color: #818182;\n}\n\n.list-group-item-dark {\n color: #1b1e21;\n background-color: #c6c8ca;\n}\n\n.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {\n color: #1b1e21;\n background-color: #b9bbbe;\n}\n\n.list-group-item-dark.list-group-item-action.active {\n color: #fff;\n background-color: #1b1e21;\n border-color: #1b1e21;\n}\n\n.close {\n float: right;\n font-size: 1.5rem;\n font-weight: 700;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n opacity: .5;\n}\n\n.close:hover {\n color: #000;\n text-decoration: none;\n}\n\n.close:not(:disabled):not(.disabled):hover, .close:not(:disabled):not(.disabled):focus {\n opacity: .75;\n}\n\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n}\n\na.close.disabled {\n pointer-events: none;\n}\n\n.toast {\n max-width: 350px;\n overflow: hidden;\n font-size: 0.875rem;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.1);\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n opacity: 0;\n border-radius: 0.25rem;\n}\n\n.toast:not(:last-child) {\n margin-bottom: 0.75rem;\n}\n\n.toast.showing {\n opacity: 1;\n}\n\n.toast.show {\n display: block;\n opacity: 1;\n}\n\n.toast.hide {\n display: none;\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n color: #6c757d;\n background-color: rgba(255, 255, 255, 0.85);\n background-clip: padding-box;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.toast-body {\n padding: 0.75rem;\n}\n\n.modal-open {\n overflow: hidden;\n}\n\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1050;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n outline: 0;\n}\n\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 0.5rem;\n pointer-events: none;\n}\n\n.modal.fade .modal-dialog {\n transition: transform 0.3s ease-out;\n transform: translate(0, -50px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .modal.fade .modal-dialog {\n transition: none;\n }\n}\n\n.modal.show .modal-dialog {\n transform: none;\n}\n\n.modal.modal-static .modal-dialog {\n transform: scale(1.02);\n}\n\n.modal-dialog-scrollable {\n display: flex;\n max-height: calc(100% - 1rem);\n}\n\n.modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 1rem);\n overflow: hidden;\n}\n\n.modal-dialog-scrollable .modal-header,\n.modal-dialog-scrollable .modal-footer {\n flex-shrink: 0;\n}\n\n.modal-dialog-scrollable .modal-body {\n overflow-y: auto;\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: calc(100% - 1rem);\n}\n\n.modal-dialog-centered::before {\n display: block;\n height: calc(100vh - 1rem);\n height: min-content;\n content: \"\";\n}\n\n.modal-dialog-centered.modal-dialog-scrollable {\n flex-direction: column;\n justify-content: center;\n height: 100%;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable .modal-content {\n max-height: none;\n}\n\n.modal-dialog-centered.modal-dialog-scrollable::before {\n content: none;\n}\n\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n pointer-events: auto;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n outline: 0;\n}\n\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: 1040;\n width: 100vw;\n height: 100vh;\n background-color: #000;\n}\n\n.modal-backdrop.fade {\n opacity: 0;\n}\n\n.modal-backdrop.show {\n opacity: 0.5;\n}\n\n.modal-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 1rem 1rem;\n border-bottom: 1px solid #dee2e6;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.modal-header .close {\n padding: 1rem 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n\n.modal-title {\n margin-bottom: 0;\n line-height: 1.5;\n}\n\n.modal-body {\n position: relative;\n flex: 1 1 auto;\n padding: 1rem;\n}\n\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n padding: 0.75rem;\n border-top: 1px solid #dee2e6;\n border-bottom-right-radius: calc(0.3rem - 1px);\n border-bottom-left-radius: calc(0.3rem - 1px);\n}\n\n.modal-footer > * {\n margin: 0.25rem;\n}\n\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n@media (min-width: 576px) {\n .modal-dialog {\n max-width: 500px;\n margin: 1.75rem auto;\n }\n .modal-dialog-scrollable {\n max-height: calc(100% - 3.5rem);\n }\n .modal-dialog-scrollable .modal-content {\n max-height: calc(100vh - 3.5rem);\n }\n .modal-dialog-centered {\n min-height: calc(100% - 3.5rem);\n }\n .modal-dialog-centered::before {\n height: calc(100vh - 3.5rem);\n height: min-content;\n }\n .modal-sm {\n max-width: 300px;\n }\n}\n\n@media (min-width: 992px) {\n .modal-lg,\n .modal-xl {\n max-width: 800px;\n }\n}\n\n@media (min-width: 1200px) {\n .modal-xl {\n max-width: 1140px;\n }\n}\n\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n margin: 0;\n 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\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n opacity: 0;\n}\n\n.tooltip.show {\n opacity: 0.9;\n}\n\n.tooltip .arrow {\n position: absolute;\n display: block;\n width: 0.8rem;\n height: 0.4rem;\n}\n\n.tooltip .arrow::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-tooltip-top, .bs-tooltip-auto[x-placement^=\"top\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-top .arrow, .bs-tooltip-auto[x-placement^=\"top\"] .arrow {\n bottom: 0;\n}\n\n.bs-tooltip-top .arrow::before, .bs-tooltip-auto[x-placement^=\"top\"] .arrow::before {\n top: 0;\n border-width: 0.4rem 0.4rem 0;\n border-top-color: #000;\n}\n\n.bs-tooltip-right, .bs-tooltip-auto[x-placement^=\"right\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-right .arrow, .bs-tooltip-auto[x-placement^=\"right\"] .arrow {\n left: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-right .arrow::before, .bs-tooltip-auto[x-placement^=\"right\"] .arrow::before {\n right: 0;\n border-width: 0.4rem 0.4rem 0.4rem 0;\n border-right-color: #000;\n}\n\n.bs-tooltip-bottom, .bs-tooltip-auto[x-placement^=\"bottom\"] {\n padding: 0.4rem 0;\n}\n\n.bs-tooltip-bottom .arrow, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow {\n top: 0;\n}\n\n.bs-tooltip-bottom .arrow::before, .bs-tooltip-auto[x-placement^=\"bottom\"] .arrow::before {\n bottom: 0;\n border-width: 0 0.4rem 0.4rem;\n border-bottom-color: #000;\n}\n\n.bs-tooltip-left, .bs-tooltip-auto[x-placement^=\"left\"] {\n padding: 0 0.4rem;\n}\n\n.bs-tooltip-left .arrow, .bs-tooltip-auto[x-placement^=\"left\"] .arrow {\n right: 0;\n width: 0.4rem;\n height: 0.8rem;\n}\n\n.bs-tooltip-left .arrow::before, .bs-tooltip-auto[x-placement^=\"left\"] .arrow::before {\n left: 0;\n border-width: 0.4rem 0 0.4rem 0.4rem;\n border-left-color: #000;\n}\n\n.tooltip-inner {\n max-width: 200px;\n padding: 0.25rem 0.5rem;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 0.25rem;\n}\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: block;\n max-width: 276px;\n 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\";\n font-style: normal;\n font-weight: 400;\n line-height: 1.5;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n font-size: 0.875rem;\n word-wrap: break-word;\n background-color: #fff;\n background-clip: padding-box;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 0.3rem;\n}\n\n.popover .arrow {\n position: absolute;\n display: block;\n width: 1rem;\n height: 0.5rem;\n margin: 0 0.3rem;\n}\n\n.popover .arrow::before, .popover .arrow::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n}\n\n.bs-popover-top, .bs-popover-auto[x-placement^=\"top\"] {\n margin-bottom: 0.5rem;\n}\n\n.bs-popover-top > .arrow, .bs-popover-auto[x-placement^=\"top\"] > .arrow {\n bottom: calc(-0.5rem - 1px);\n}\n\n.bs-popover-top > .arrow::before, .bs-popover-auto[x-placement^=\"top\"] > .arrow::before {\n bottom: 0;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-top > .arrow::after, .bs-popover-auto[x-placement^=\"top\"] > .arrow::after {\n bottom: 1px;\n border-width: 0.5rem 0.5rem 0;\n border-top-color: #fff;\n}\n\n.bs-popover-right, .bs-popover-auto[x-placement^=\"right\"] {\n margin-left: 0.5rem;\n}\n\n.bs-popover-right > .arrow, .bs-popover-auto[x-placement^=\"right\"] > .arrow {\n left: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-right > .arrow::before, .bs-popover-auto[x-placement^=\"right\"] > .arrow::before {\n left: 0;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-right > .arrow::after, .bs-popover-auto[x-placement^=\"right\"] > .arrow::after {\n left: 1px;\n border-width: 0.5rem 0.5rem 0.5rem 0;\n border-right-color: #fff;\n}\n\n.bs-popover-bottom, .bs-popover-auto[x-placement^=\"bottom\"] {\n margin-top: 0.5rem;\n}\n\n.bs-popover-bottom > .arrow, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow {\n top: calc(-0.5rem - 1px);\n}\n\n.bs-popover-bottom > .arrow::before, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::before {\n top: 0;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-bottom > .arrow::after, .bs-popover-auto[x-placement^=\"bottom\"] > .arrow::after {\n top: 1px;\n border-width: 0 0.5rem 0.5rem 0.5rem;\n border-bottom-color: #fff;\n}\n\n.bs-popover-bottom .popover-header::before, .bs-popover-auto[x-placement^=\"bottom\"] .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: 1rem;\n margin-left: -0.5rem;\n content: \"\";\n border-bottom: 1px solid #f7f7f7;\n}\n\n.bs-popover-left, .bs-popover-auto[x-placement^=\"left\"] {\n margin-right: 0.5rem;\n}\n\n.bs-popover-left > .arrow, .bs-popover-auto[x-placement^=\"left\"] > .arrow {\n right: calc(-0.5rem - 1px);\n width: 0.5rem;\n height: 1rem;\n margin: 0.3rem 0;\n}\n\n.bs-popover-left > .arrow::before, .bs-popover-auto[x-placement^=\"left\"] > .arrow::before {\n right: 0;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n\n.bs-popover-left > .arrow::after, .bs-popover-auto[x-placement^=\"left\"] > .arrow::after {\n right: 1px;\n border-width: 0.5rem 0 0.5rem 0.5rem;\n border-left-color: #fff;\n}\n\n.popover-header {\n padding: 0.5rem 0.75rem;\n margin-bottom: 0;\n font-size: 1rem;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-top-left-radius: calc(0.3rem - 1px);\n border-top-right-radius: calc(0.3rem - 1px);\n}\n\n.popover-header:empty {\n display: none;\n}\n\n.popover-body {\n padding: 0.5rem 0.75rem;\n color: #212529;\n}\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.carousel-inner::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n transition: transform 0.6s ease-in-out;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-item {\n transition: none;\n }\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n.carousel-fade .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n}\n\n.carousel-fade .carousel-item.active,\n.carousel-fade .carousel-item-next.carousel-item-left,\n.carousel-fade .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n}\n\n.carousel-fade .active.carousel-item-left,\n.carousel-fade .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n transition: opacity 0s 0.6s;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-fade .active.carousel-item-left,\n .carousel-fade .active.carousel-item-right {\n transition: none;\n }\n}\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 15%;\n color: #fff;\n text-align: center;\n opacity: 0.5;\n transition: opacity 0.15s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-control-prev,\n .carousel-control-next {\n transition: none;\n }\n}\n\n.carousel-control-prev:hover, .carousel-control-prev:focus,\n.carousel-control-next:hover,\n.carousel-control-next:focus {\n color: #fff;\n text-decoration: none;\n outline: 0;\n opacity: 0.9;\n}\n\n.carousel-control-prev {\n left: 0;\n}\n\n.carousel-control-next {\n right: 0;\n}\n\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: 20px;\n height: 20px;\n background: no-repeat 50% / 100% 100%;\n}\n\n.carousel-control-prev-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' 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\");\n}\n\n.carousel-control-next-icon {\n background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' 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\");\n}\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0;\n margin-right: 15%;\n margin-left: 15%;\n list-style: none;\n}\n\n.carousel-indicators li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: 30px;\n height: 3px;\n margin-right: 3px;\n margin-left: 3px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #fff;\n background-clip: padding-box;\n border-top: 10px solid transparent;\n border-bottom: 10px solid transparent;\n opacity: .5;\n transition: opacity 0.6s ease;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .carousel-indicators li {\n transition: none;\n }\n}\n\n.carousel-indicators .active {\n opacity: 1;\n}\n\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n}\n\n@keyframes spinner-border {\n to {\n transform: rotate(360deg);\n }\n}\n\n.spinner-border {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n border: 0.25em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: 1rem;\n height: 1rem;\n border-width: 0.2em;\n}\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: 2rem;\n height: 2rem;\n vertical-align: text-bottom;\n background-color: currentColor;\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: 1rem;\n height: 1rem;\n}\n\n.align-baseline {\n vertical-align: baseline !important;\n}\n\n.align-top {\n vertical-align: top !important;\n}\n\n.align-middle {\n vertical-align: middle !important;\n}\n\n.align-bottom {\n vertical-align: bottom !important;\n}\n\n.align-text-bottom {\n vertical-align: text-bottom !important;\n}\n\n.align-text-top {\n vertical-align: text-top !important;\n}\n\n.bg-primary {\n background-color: #007bff !important;\n}\n\na.bg-primary:hover, a.bg-primary:focus,\nbutton.bg-primary:hover,\nbutton.bg-primary:focus {\n background-color: #0062cc !important;\n}\n\n.bg-secondary {\n background-color: #6c757d !important;\n}\n\na.bg-secondary:hover, a.bg-secondary:focus,\nbutton.bg-secondary:hover,\nbutton.bg-secondary:focus {\n background-color: #545b62 !important;\n}\n\n.bg-success {\n background-color: #28a745 !important;\n}\n\na.bg-success:hover, a.bg-success:focus,\nbutton.bg-success:hover,\nbutton.bg-success:focus {\n background-color: #1e7e34 !important;\n}\n\n.bg-info {\n background-color: #17a2b8 !important;\n}\n\na.bg-info:hover, a.bg-info:focus,\nbutton.bg-info:hover,\nbutton.bg-info:focus {\n background-color: #117a8b !important;\n}\n\n.bg-warning {\n background-color: #ffc107 !important;\n}\n\na.bg-warning:hover, a.bg-warning:focus,\nbutton.bg-warning:hover,\nbutton.bg-warning:focus {\n background-color: #d39e00 !important;\n}\n\n.bg-danger {\n background-color: #dc3545 !important;\n}\n\na.bg-danger:hover, a.bg-danger:focus,\nbutton.bg-danger:hover,\nbutton.bg-danger:focus {\n background-color: #bd2130 !important;\n}\n\n.bg-light {\n background-color: #f8f9fa !important;\n}\n\na.bg-light:hover, a.bg-light:focus,\nbutton.bg-light:hover,\nbutton.bg-light:focus {\n background-color: #dae0e5 !important;\n}\n\n.bg-dark {\n background-color: #343a40 !important;\n}\n\na.bg-dark:hover, a.bg-dark:focus,\nbutton.bg-dark:hover,\nbutton.bg-dark:focus {\n background-color: #1d2124 !important;\n}\n\n.bg-white {\n background-color: #fff !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n\n.border {\n border: 1px solid #dee2e6 !important;\n}\n\n.border-top {\n border-top: 1px solid #dee2e6 !important;\n}\n\n.border-right {\n border-right: 1px solid #dee2e6 !important;\n}\n\n.border-bottom {\n border-bottom: 1px solid #dee2e6 !important;\n}\n\n.border-left {\n border-left: 1px solid #dee2e6 !important;\n}\n\n.border-0 {\n border: 0 !important;\n}\n\n.border-top-0 {\n border-top: 0 !important;\n}\n\n.border-right-0 {\n border-right: 0 !important;\n}\n\n.border-bottom-0 {\n border-bottom: 0 !important;\n}\n\n.border-left-0 {\n border-left: 0 !important;\n}\n\n.border-primary {\n border-color: #007bff !important;\n}\n\n.border-secondary {\n border-color: #6c757d !important;\n}\n\n.border-success {\n border-color: #28a745 !important;\n}\n\n.border-info {\n border-color: #17a2b8 !important;\n}\n\n.border-warning {\n border-color: #ffc107 !important;\n}\n\n.border-danger {\n border-color: #dc3545 !important;\n}\n\n.border-light {\n border-color: #f8f9fa !important;\n}\n\n.border-dark {\n border-color: #343a40 !important;\n}\n\n.border-white {\n border-color: #fff !important;\n}\n\n.rounded-sm {\n border-radius: 0.2rem !important;\n}\n\n.rounded {\n border-radius: 0.25rem !important;\n}\n\n.rounded-top {\n border-top-left-radius: 0.25rem !important;\n border-top-right-radius: 0.25rem !important;\n}\n\n.rounded-right {\n border-top-right-radius: 0.25rem !important;\n border-bottom-right-radius: 0.25rem !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-left {\n border-top-left-radius: 0.25rem !important;\n border-bottom-left-radius: 0.25rem !important;\n}\n\n.rounded-lg {\n border-radius: 0.3rem !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: 50rem !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n\n.clearfix::after {\n display: block;\n clear: both;\n content: \"\";\n}\n\n.d-none {\n display: none !important;\n}\n\n.d-inline {\n display: inline !important;\n}\n\n.d-inline-block {\n display: inline-block !important;\n}\n\n.d-block {\n display: block !important;\n}\n\n.d-table {\n display: table !important;\n}\n\n.d-table-row {\n display: table-row !important;\n}\n\n.d-table-cell {\n display: table-cell !important;\n}\n\n.d-flex {\n display: flex !important;\n}\n\n.d-inline-flex {\n display: inline-flex !important;\n}\n\n@media (min-width: 576px) {\n .d-sm-none {\n display: none !important;\n }\n .d-sm-inline {\n display: inline !important;\n }\n .d-sm-inline-block {\n display: inline-block !important;\n }\n .d-sm-block {\n display: block !important;\n }\n .d-sm-table {\n display: table !important;\n }\n .d-sm-table-row {\n display: table-row !important;\n }\n .d-sm-table-cell {\n display: table-cell !important;\n }\n .d-sm-flex {\n display: flex !important;\n }\n .d-sm-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 768px) {\n .d-md-none {\n display: none !important;\n }\n .d-md-inline {\n display: inline !important;\n }\n .d-md-inline-block {\n display: inline-block !important;\n }\n .d-md-block {\n display: block !important;\n }\n .d-md-table {\n display: table !important;\n }\n .d-md-table-row {\n display: table-row !important;\n }\n .d-md-table-cell {\n display: table-cell !important;\n }\n .d-md-flex {\n display: flex !important;\n }\n .d-md-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 992px) {\n .d-lg-none {\n display: none !important;\n }\n .d-lg-inline {\n display: inline !important;\n }\n .d-lg-inline-block {\n display: inline-block !important;\n }\n .d-lg-block {\n display: block !important;\n }\n .d-lg-table {\n display: table !important;\n }\n .d-lg-table-row {\n display: table-row !important;\n }\n .d-lg-table-cell {\n display: table-cell !important;\n }\n .d-lg-flex {\n display: flex !important;\n }\n .d-lg-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media (min-width: 1200px) {\n .d-xl-none {\n display: none !important;\n }\n .d-xl-inline {\n display: inline !important;\n }\n .d-xl-inline-block {\n display: inline-block !important;\n }\n .d-xl-block {\n display: block !important;\n }\n .d-xl-table {\n display: table !important;\n }\n .d-xl-table-row {\n display: table-row !important;\n }\n .d-xl-table-cell {\n display: table-cell !important;\n }\n .d-xl-flex {\n display: flex !important;\n }\n .d-xl-inline-flex {\n display: inline-flex !important;\n }\n}\n\n@media print {\n .d-print-none {\n display: none !important;\n }\n .d-print-inline {\n display: inline !important;\n }\n .d-print-inline-block {\n display: inline-block !important;\n }\n .d-print-block {\n display: block !important;\n }\n .d-print-table {\n display: table !important;\n }\n .d-print-table-row {\n display: table-row !important;\n }\n .d-print-table-cell {\n display: table-cell !important;\n }\n .d-print-flex {\n display: flex !important;\n }\n .d-print-inline-flex {\n display: inline-flex !important;\n }\n}\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n}\n\n.embed-responsive::before {\n display: block;\n content: \"\";\n}\n\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n\n.embed-responsive-21by9::before {\n padding-top: 42.857143%;\n}\n\n.embed-responsive-16by9::before {\n padding-top: 56.25%;\n}\n\n.embed-responsive-4by3::before {\n padding-top: 75%;\n}\n\n.embed-responsive-1by1::before {\n padding-top: 100%;\n}\n\n.flex-row {\n flex-direction: row !important;\n}\n\n.flex-column {\n flex-direction: column !important;\n}\n\n.flex-row-reverse {\n flex-direction: row-reverse !important;\n}\n\n.flex-column-reverse {\n flex-direction: column-reverse !important;\n}\n\n.flex-wrap {\n flex-wrap: wrap !important;\n}\n\n.flex-nowrap {\n flex-wrap: nowrap !important;\n}\n\n.flex-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n}\n\n.flex-fill {\n flex: 1 1 auto !important;\n}\n\n.flex-grow-0 {\n flex-grow: 0 !important;\n}\n\n.flex-grow-1 {\n flex-grow: 1 !important;\n}\n\n.flex-shrink-0 {\n flex-shrink: 0 !important;\n}\n\n.flex-shrink-1 {\n flex-shrink: 1 !important;\n}\n\n.justify-content-start {\n justify-content: flex-start !important;\n}\n\n.justify-content-end {\n justify-content: flex-end !important;\n}\n\n.justify-content-center {\n justify-content: center !important;\n}\n\n.justify-content-between {\n justify-content: space-between !important;\n}\n\n.justify-content-around {\n justify-content: space-around !important;\n}\n\n.align-items-start {\n align-items: flex-start !important;\n}\n\n.align-items-end {\n align-items: flex-end !important;\n}\n\n.align-items-center {\n align-items: center !important;\n}\n\n.align-items-baseline {\n align-items: baseline !important;\n}\n\n.align-items-stretch {\n align-items: stretch !important;\n}\n\n.align-content-start {\n align-content: flex-start !important;\n}\n\n.align-content-end {\n align-content: flex-end !important;\n}\n\n.align-content-center {\n align-content: center !important;\n}\n\n.align-content-between {\n align-content: space-between !important;\n}\n\n.align-content-around {\n align-content: space-around !important;\n}\n\n.align-content-stretch {\n align-content: stretch !important;\n}\n\n.align-self-auto {\n align-self: auto !important;\n}\n\n.align-self-start {\n align-self: flex-start !important;\n}\n\n.align-self-end {\n align-self: flex-end !important;\n}\n\n.align-self-center {\n align-self: center !important;\n}\n\n.align-self-baseline {\n align-self: baseline !important;\n}\n\n.align-self-stretch {\n align-self: stretch !important;\n}\n\n@media (min-width: 576px) {\n .flex-sm-row {\n flex-direction: row !important;\n }\n .flex-sm-column {\n flex-direction: column !important;\n }\n .flex-sm-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-sm-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-sm-wrap {\n flex-wrap: wrap !important;\n }\n .flex-sm-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-sm-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-sm-fill {\n flex: 1 1 auto !important;\n }\n .flex-sm-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-sm-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-sm-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-sm-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-sm-start {\n justify-content: flex-start !important;\n }\n .justify-content-sm-end {\n justify-content: flex-end !important;\n }\n .justify-content-sm-center {\n justify-content: center !important;\n }\n .justify-content-sm-between {\n justify-content: space-between !important;\n }\n .justify-content-sm-around {\n justify-content: space-around !important;\n }\n .align-items-sm-start {\n align-items: flex-start !important;\n }\n .align-items-sm-end {\n align-items: flex-end !important;\n }\n .align-items-sm-center {\n align-items: center !important;\n }\n .align-items-sm-baseline {\n align-items: baseline !important;\n }\n .align-items-sm-stretch {\n align-items: stretch !important;\n }\n .align-content-sm-start {\n align-content: flex-start !important;\n }\n .align-content-sm-end {\n align-content: flex-end !important;\n }\n .align-content-sm-center {\n align-content: center !important;\n }\n .align-content-sm-between {\n align-content: space-between !important;\n }\n .align-content-sm-around {\n align-content: space-around !important;\n }\n .align-content-sm-stretch {\n align-content: stretch !important;\n }\n .align-self-sm-auto {\n align-self: auto !important;\n }\n .align-self-sm-start {\n align-self: flex-start !important;\n }\n .align-self-sm-end {\n align-self: flex-end !important;\n }\n .align-self-sm-center {\n align-self: center !important;\n }\n .align-self-sm-baseline {\n align-self: baseline !important;\n }\n .align-self-sm-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 768px) {\n .flex-md-row {\n flex-direction: row !important;\n }\n .flex-md-column {\n flex-direction: column !important;\n }\n .flex-md-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-md-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-md-wrap {\n flex-wrap: wrap !important;\n }\n .flex-md-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-md-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-md-fill {\n flex: 1 1 auto !important;\n }\n .flex-md-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-md-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-md-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-md-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-md-start {\n justify-content: flex-start !important;\n }\n .justify-content-md-end {\n justify-content: flex-end !important;\n }\n .justify-content-md-center {\n justify-content: center !important;\n }\n .justify-content-md-between {\n justify-content: space-between !important;\n }\n .justify-content-md-around {\n justify-content: space-around !important;\n }\n .align-items-md-start {\n align-items: flex-start !important;\n }\n .align-items-md-end {\n align-items: flex-end !important;\n }\n .align-items-md-center {\n align-items: center !important;\n }\n .align-items-md-baseline {\n align-items: baseline !important;\n }\n .align-items-md-stretch {\n align-items: stretch !important;\n }\n .align-content-md-start {\n align-content: flex-start !important;\n }\n .align-content-md-end {\n align-content: flex-end !important;\n }\n .align-content-md-center {\n align-content: center !important;\n }\n .align-content-md-between {\n align-content: space-between !important;\n }\n .align-content-md-around {\n align-content: space-around !important;\n }\n .align-content-md-stretch {\n align-content: stretch !important;\n }\n .align-self-md-auto {\n align-self: auto !important;\n }\n .align-self-md-start {\n align-self: flex-start !important;\n }\n .align-self-md-end {\n align-self: flex-end !important;\n }\n .align-self-md-center {\n align-self: center !important;\n }\n .align-self-md-baseline {\n align-self: baseline !important;\n }\n .align-self-md-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 992px) {\n .flex-lg-row {\n flex-direction: row !important;\n }\n .flex-lg-column {\n flex-direction: column !important;\n }\n .flex-lg-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-lg-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-lg-wrap {\n flex-wrap: wrap !important;\n }\n .flex-lg-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-lg-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-lg-fill {\n flex: 1 1 auto !important;\n }\n .flex-lg-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-lg-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-lg-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-lg-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-lg-start {\n justify-content: flex-start !important;\n }\n .justify-content-lg-end {\n justify-content: flex-end !important;\n }\n .justify-content-lg-center {\n justify-content: center !important;\n }\n .justify-content-lg-between {\n justify-content: space-between !important;\n }\n .justify-content-lg-around {\n justify-content: space-around !important;\n }\n .align-items-lg-start {\n align-items: flex-start !important;\n }\n .align-items-lg-end {\n align-items: flex-end !important;\n }\n .align-items-lg-center {\n align-items: center !important;\n }\n .align-items-lg-baseline {\n align-items: baseline !important;\n }\n .align-items-lg-stretch {\n align-items: stretch !important;\n }\n .align-content-lg-start {\n align-content: flex-start !important;\n }\n .align-content-lg-end {\n align-content: flex-end !important;\n }\n .align-content-lg-center {\n align-content: center !important;\n }\n .align-content-lg-between {\n align-content: space-between !important;\n }\n .align-content-lg-around {\n align-content: space-around !important;\n }\n .align-content-lg-stretch {\n align-content: stretch !important;\n }\n .align-self-lg-auto {\n align-self: auto !important;\n }\n .align-self-lg-start {\n align-self: flex-start !important;\n }\n .align-self-lg-end {\n align-self: flex-end !important;\n }\n .align-self-lg-center {\n align-self: center !important;\n }\n .align-self-lg-baseline {\n align-self: baseline !important;\n }\n .align-self-lg-stretch {\n align-self: stretch !important;\n }\n}\n\n@media (min-width: 1200px) {\n .flex-xl-row {\n flex-direction: row !important;\n }\n .flex-xl-column {\n flex-direction: column !important;\n }\n .flex-xl-row-reverse {\n flex-direction: row-reverse !important;\n }\n .flex-xl-column-reverse {\n flex-direction: column-reverse !important;\n }\n .flex-xl-wrap {\n flex-wrap: wrap !important;\n }\n .flex-xl-nowrap {\n flex-wrap: nowrap !important;\n }\n .flex-xl-wrap-reverse {\n flex-wrap: wrap-reverse !important;\n }\n .flex-xl-fill {\n flex: 1 1 auto !important;\n }\n .flex-xl-grow-0 {\n flex-grow: 0 !important;\n }\n .flex-xl-grow-1 {\n flex-grow: 1 !important;\n }\n .flex-xl-shrink-0 {\n flex-shrink: 0 !important;\n }\n .flex-xl-shrink-1 {\n flex-shrink: 1 !important;\n }\n .justify-content-xl-start {\n justify-content: flex-start !important;\n }\n .justify-content-xl-end {\n justify-content: flex-end !important;\n }\n .justify-content-xl-center {\n justify-content: center !important;\n }\n .justify-content-xl-between {\n justify-content: space-between !important;\n }\n .justify-content-xl-around {\n justify-content: space-around !important;\n }\n .align-items-xl-start {\n align-items: flex-start !important;\n }\n .align-items-xl-end {\n align-items: flex-end !important;\n }\n .align-items-xl-center {\n align-items: center !important;\n }\n .align-items-xl-baseline {\n align-items: baseline !important;\n }\n .align-items-xl-stretch {\n align-items: stretch !important;\n }\n .align-content-xl-start {\n align-content: flex-start !important;\n }\n .align-content-xl-end {\n align-content: flex-end !important;\n }\n .align-content-xl-center {\n align-content: center !important;\n }\n .align-content-xl-between {\n align-content: space-between !important;\n }\n .align-content-xl-around {\n align-content: space-around !important;\n }\n .align-content-xl-stretch {\n align-content: stretch !important;\n }\n .align-self-xl-auto {\n align-self: auto !important;\n }\n .align-self-xl-start {\n align-self: flex-start !important;\n }\n .align-self-xl-end {\n align-self: flex-end !important;\n }\n .align-self-xl-center {\n align-self: center !important;\n }\n .align-self-xl-baseline {\n align-self: baseline !important;\n }\n .align-self-xl-stretch {\n align-self: stretch !important;\n }\n}\n\n.float-left {\n float: left !important;\n}\n\n.float-right {\n float: right !important;\n}\n\n.float-none {\n float: none !important;\n}\n\n@media (min-width: 576px) {\n .float-sm-left {\n float: left !important;\n }\n .float-sm-right {\n float: right !important;\n }\n .float-sm-none {\n float: none !important;\n }\n}\n\n@media (min-width: 768px) {\n .float-md-left {\n float: left !important;\n }\n .float-md-right {\n float: right !important;\n }\n .float-md-none {\n float: none !important;\n }\n}\n\n@media (min-width: 992px) {\n .float-lg-left {\n float: left !important;\n }\n .float-lg-right {\n float: right !important;\n }\n .float-lg-none {\n float: none !important;\n }\n}\n\n@media (min-width: 1200px) {\n .float-xl-left {\n float: left !important;\n }\n .float-xl-right {\n float: right !important;\n }\n .float-xl-none {\n float: none !important;\n }\n}\n\n.user-select-all {\n user-select: all !important;\n}\n\n.user-select-auto {\n user-select: auto !important;\n}\n\n.user-select-none {\n user-select: none !important;\n}\n\n.overflow-auto {\n overflow: auto !important;\n}\n\n.overflow-hidden {\n overflow: hidden !important;\n}\n\n.position-static {\n position: static !important;\n}\n\n.position-relative {\n position: relative !important;\n}\n\n.position-absolute {\n position: absolute !important;\n}\n\n.position-fixed {\n position: fixed !important;\n}\n\n.position-sticky {\n position: sticky !important;\n}\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1030;\n}\n\n@supports (position: sticky) {\n .sticky-top {\n position: sticky;\n top: 0;\n z-index: 1020;\n }\n}\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n.sr-only-focusable:active, .sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n}\n\n.shadow-sm {\n box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;\n}\n\n.shadow {\n box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;\n}\n\n.shadow-lg {\n box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;\n}\n\n.shadow-none {\n box-shadow: none !important;\n}\n\n.w-25 {\n width: 25% !important;\n}\n\n.w-50 {\n width: 50% !important;\n}\n\n.w-75 {\n width: 75% !important;\n}\n\n.w-100 {\n width: 100% !important;\n}\n\n.w-auto {\n width: auto !important;\n}\n\n.h-25 {\n height: 25% !important;\n}\n\n.h-50 {\n height: 50% !important;\n}\n\n.h-75 {\n height: 75% !important;\n}\n\n.h-100 {\n height: 100% !important;\n}\n\n.h-auto {\n height: auto !important;\n}\n\n.mw-100 {\n max-width: 100% !important;\n}\n\n.mh-100 {\n max-height: 100% !important;\n}\n\n.min-vw-100 {\n min-width: 100vw !important;\n}\n\n.min-vh-100 {\n min-height: 100vh !important;\n}\n\n.vw-100 {\n width: 100vw !important;\n}\n\n.vh-100 {\n height: 100vh !important;\n}\n\n.m-0 {\n margin: 0 !important;\n}\n\n.mt-0,\n.my-0 {\n margin-top: 0 !important;\n}\n\n.mr-0,\n.mx-0 {\n margin-right: 0 !important;\n}\n\n.mb-0,\n.my-0 {\n margin-bottom: 0 !important;\n}\n\n.ml-0,\n.mx-0 {\n margin-left: 0 !important;\n}\n\n.m-1 {\n margin: 0.25rem !important;\n}\n\n.mt-1,\n.my-1 {\n margin-top: 0.25rem !important;\n}\n\n.mr-1,\n.mx-1 {\n margin-right: 0.25rem !important;\n}\n\n.mb-1,\n.my-1 {\n margin-bottom: 0.25rem !important;\n}\n\n.ml-1,\n.mx-1 {\n margin-left: 0.25rem !important;\n}\n\n.m-2 {\n margin: 0.5rem !important;\n}\n\n.mt-2,\n.my-2 {\n margin-top: 0.5rem !important;\n}\n\n.mr-2,\n.mx-2 {\n margin-right: 0.5rem !important;\n}\n\n.mb-2,\n.my-2 {\n margin-bottom: 0.5rem !important;\n}\n\n.ml-2,\n.mx-2 {\n margin-left: 0.5rem !important;\n}\n\n.m-3 {\n margin: 1rem !important;\n}\n\n.mt-3,\n.my-3 {\n margin-top: 1rem !important;\n}\n\n.mr-3,\n.mx-3 {\n margin-right: 1rem !important;\n}\n\n.mb-3,\n.my-3 {\n margin-bottom: 1rem !important;\n}\n\n.ml-3,\n.mx-3 {\n margin-left: 1rem !important;\n}\n\n.m-4 {\n margin: 1.5rem !important;\n}\n\n.mt-4,\n.my-4 {\n margin-top: 1.5rem !important;\n}\n\n.mr-4,\n.mx-4 {\n margin-right: 1.5rem !important;\n}\n\n.mb-4,\n.my-4 {\n margin-bottom: 1.5rem !important;\n}\n\n.ml-4,\n.mx-4 {\n margin-left: 1.5rem !important;\n}\n\n.m-5 {\n margin: 3rem !important;\n}\n\n.mt-5,\n.my-5 {\n margin-top: 3rem !important;\n}\n\n.mr-5,\n.mx-5 {\n margin-right: 3rem !important;\n}\n\n.mb-5,\n.my-5 {\n margin-bottom: 3rem !important;\n}\n\n.ml-5,\n.mx-5 {\n margin-left: 3rem !important;\n}\n\n.p-0 {\n padding: 0 !important;\n}\n\n.pt-0,\n.py-0 {\n padding-top: 0 !important;\n}\n\n.pr-0,\n.px-0 {\n padding-right: 0 !important;\n}\n\n.pb-0,\n.py-0 {\n padding-bottom: 0 !important;\n}\n\n.pl-0,\n.px-0 {\n padding-left: 0 !important;\n}\n\n.p-1 {\n padding: 0.25rem !important;\n}\n\n.pt-1,\n.py-1 {\n padding-top: 0.25rem !important;\n}\n\n.pr-1,\n.px-1 {\n padding-right: 0.25rem !important;\n}\n\n.pb-1,\n.py-1 {\n padding-bottom: 0.25rem !important;\n}\n\n.pl-1,\n.px-1 {\n padding-left: 0.25rem !important;\n}\n\n.p-2 {\n padding: 0.5rem !important;\n}\n\n.pt-2,\n.py-2 {\n padding-top: 0.5rem !important;\n}\n\n.pr-2,\n.px-2 {\n padding-right: 0.5rem !important;\n}\n\n.pb-2,\n.py-2 {\n padding-bottom: 0.5rem !important;\n}\n\n.pl-2,\n.px-2 {\n padding-left: 0.5rem !important;\n}\n\n.p-3 {\n padding: 1rem !important;\n}\n\n.pt-3,\n.py-3 {\n padding-top: 1rem !important;\n}\n\n.pr-3,\n.px-3 {\n padding-right: 1rem !important;\n}\n\n.pb-3,\n.py-3 {\n padding-bottom: 1rem !important;\n}\n\n.pl-3,\n.px-3 {\n padding-left: 1rem !important;\n}\n\n.p-4 {\n padding: 1.5rem !important;\n}\n\n.pt-4,\n.py-4 {\n padding-top: 1.5rem !important;\n}\n\n.pr-4,\n.px-4 {\n padding-right: 1.5rem !important;\n}\n\n.pb-4,\n.py-4 {\n padding-bottom: 1.5rem !important;\n}\n\n.pl-4,\n.px-4 {\n padding-left: 1.5rem !important;\n}\n\n.p-5 {\n padding: 3rem !important;\n}\n\n.pt-5,\n.py-5 {\n padding-top: 3rem !important;\n}\n\n.pr-5,\n.px-5 {\n padding-right: 3rem !important;\n}\n\n.pb-5,\n.py-5 {\n padding-bottom: 3rem !important;\n}\n\n.pl-5,\n.px-5 {\n padding-left: 3rem !important;\n}\n\n.m-n1 {\n margin: -0.25rem !important;\n}\n\n.mt-n1,\n.my-n1 {\n margin-top: -0.25rem !important;\n}\n\n.mr-n1,\n.mx-n1 {\n margin-right: -0.25rem !important;\n}\n\n.mb-n1,\n.my-n1 {\n margin-bottom: -0.25rem !important;\n}\n\n.ml-n1,\n.mx-n1 {\n margin-left: -0.25rem !important;\n}\n\n.m-n2 {\n margin: -0.5rem !important;\n}\n\n.mt-n2,\n.my-n2 {\n margin-top: -0.5rem !important;\n}\n\n.mr-n2,\n.mx-n2 {\n margin-right: -0.5rem !important;\n}\n\n.mb-n2,\n.my-n2 {\n margin-bottom: -0.5rem !important;\n}\n\n.ml-n2,\n.mx-n2 {\n margin-left: -0.5rem !important;\n}\n\n.m-n3 {\n margin: -1rem !important;\n}\n\n.mt-n3,\n.my-n3 {\n margin-top: -1rem !important;\n}\n\n.mr-n3,\n.mx-n3 {\n margin-right: -1rem !important;\n}\n\n.mb-n3,\n.my-n3 {\n margin-bottom: -1rem !important;\n}\n\n.ml-n3,\n.mx-n3 {\n margin-left: -1rem !important;\n}\n\n.m-n4 {\n margin: -1.5rem !important;\n}\n\n.mt-n4,\n.my-n4 {\n margin-top: -1.5rem !important;\n}\n\n.mr-n4,\n.mx-n4 {\n margin-right: -1.5rem !important;\n}\n\n.mb-n4,\n.my-n4 {\n margin-bottom: -1.5rem !important;\n}\n\n.ml-n4,\n.mx-n4 {\n margin-left: -1.5rem !important;\n}\n\n.m-n5 {\n margin: -3rem !important;\n}\n\n.mt-n5,\n.my-n5 {\n margin-top: -3rem !important;\n}\n\n.mr-n5,\n.mx-n5 {\n margin-right: -3rem !important;\n}\n\n.mb-n5,\n.my-n5 {\n margin-bottom: -3rem !important;\n}\n\n.ml-n5,\n.mx-n5 {\n margin-left: -3rem !important;\n}\n\n.m-auto {\n margin: auto !important;\n}\n\n.mt-auto,\n.my-auto {\n margin-top: auto !important;\n}\n\n.mr-auto,\n.mx-auto {\n margin-right: auto !important;\n}\n\n.mb-auto,\n.my-auto {\n margin-bottom: auto !important;\n}\n\n.ml-auto,\n.mx-auto {\n margin-left: auto !important;\n}\n\n@media (min-width: 576px) {\n .m-sm-0 {\n margin: 0 !important;\n }\n .mt-sm-0,\n .my-sm-0 {\n margin-top: 0 !important;\n }\n .mr-sm-0,\n .mx-sm-0 {\n margin-right: 0 !important;\n }\n .mb-sm-0,\n .my-sm-0 {\n margin-bottom: 0 !important;\n }\n .ml-sm-0,\n .mx-sm-0 {\n margin-left: 0 !important;\n }\n .m-sm-1 {\n margin: 0.25rem !important;\n }\n .mt-sm-1,\n .my-sm-1 {\n margin-top: 0.25rem !important;\n }\n .mr-sm-1,\n .mx-sm-1 {\n margin-right: 0.25rem !important;\n }\n .mb-sm-1,\n .my-sm-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-sm-1,\n .mx-sm-1 {\n margin-left: 0.25rem !important;\n }\n .m-sm-2 {\n margin: 0.5rem !important;\n }\n .mt-sm-2,\n .my-sm-2 {\n margin-top: 0.5rem !important;\n }\n .mr-sm-2,\n .mx-sm-2 {\n margin-right: 0.5rem !important;\n }\n .mb-sm-2,\n .my-sm-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-sm-2,\n .mx-sm-2 {\n margin-left: 0.5rem !important;\n }\n .m-sm-3 {\n margin: 1rem !important;\n }\n .mt-sm-3,\n .my-sm-3 {\n margin-top: 1rem !important;\n }\n .mr-sm-3,\n .mx-sm-3 {\n margin-right: 1rem !important;\n }\n .mb-sm-3,\n .my-sm-3 {\n margin-bottom: 1rem !important;\n }\n .ml-sm-3,\n .mx-sm-3 {\n margin-left: 1rem !important;\n }\n .m-sm-4 {\n margin: 1.5rem !important;\n }\n .mt-sm-4,\n .my-sm-4 {\n margin-top: 1.5rem !important;\n }\n .mr-sm-4,\n .mx-sm-4 {\n margin-right: 1.5rem !important;\n }\n .mb-sm-4,\n .my-sm-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-sm-4,\n .mx-sm-4 {\n margin-left: 1.5rem !important;\n }\n .m-sm-5 {\n margin: 3rem !important;\n }\n .mt-sm-5,\n .my-sm-5 {\n margin-top: 3rem !important;\n }\n .mr-sm-5,\n .mx-sm-5 {\n margin-right: 3rem !important;\n }\n .mb-sm-5,\n .my-sm-5 {\n margin-bottom: 3rem !important;\n }\n .ml-sm-5,\n .mx-sm-5 {\n margin-left: 3rem !important;\n }\n .p-sm-0 {\n padding: 0 !important;\n }\n .pt-sm-0,\n .py-sm-0 {\n padding-top: 0 !important;\n }\n .pr-sm-0,\n .px-sm-0 {\n padding-right: 0 !important;\n }\n .pb-sm-0,\n .py-sm-0 {\n padding-bottom: 0 !important;\n }\n .pl-sm-0,\n .px-sm-0 {\n padding-left: 0 !important;\n }\n .p-sm-1 {\n padding: 0.25rem !important;\n }\n .pt-sm-1,\n .py-sm-1 {\n padding-top: 0.25rem !important;\n }\n .pr-sm-1,\n .px-sm-1 {\n padding-right: 0.25rem !important;\n }\n .pb-sm-1,\n .py-sm-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-sm-1,\n .px-sm-1 {\n padding-left: 0.25rem !important;\n }\n .p-sm-2 {\n padding: 0.5rem !important;\n }\n .pt-sm-2,\n .py-sm-2 {\n padding-top: 0.5rem !important;\n }\n .pr-sm-2,\n .px-sm-2 {\n padding-right: 0.5rem !important;\n }\n .pb-sm-2,\n .py-sm-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-sm-2,\n .px-sm-2 {\n padding-left: 0.5rem !important;\n }\n .p-sm-3 {\n padding: 1rem !important;\n }\n .pt-sm-3,\n .py-sm-3 {\n padding-top: 1rem !important;\n }\n .pr-sm-3,\n .px-sm-3 {\n padding-right: 1rem !important;\n }\n .pb-sm-3,\n .py-sm-3 {\n padding-bottom: 1rem !important;\n }\n .pl-sm-3,\n .px-sm-3 {\n padding-left: 1rem !important;\n }\n .p-sm-4 {\n padding: 1.5rem !important;\n }\n .pt-sm-4,\n .py-sm-4 {\n padding-top: 1.5rem !important;\n }\n .pr-sm-4,\n .px-sm-4 {\n padding-right: 1.5rem !important;\n }\n .pb-sm-4,\n .py-sm-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-sm-4,\n .px-sm-4 {\n padding-left: 1.5rem !important;\n }\n .p-sm-5 {\n padding: 3rem !important;\n }\n .pt-sm-5,\n .py-sm-5 {\n padding-top: 3rem !important;\n }\n .pr-sm-5,\n .px-sm-5 {\n padding-right: 3rem !important;\n }\n .pb-sm-5,\n .py-sm-5 {\n padding-bottom: 3rem !important;\n }\n .pl-sm-5,\n .px-sm-5 {\n padding-left: 3rem !important;\n }\n .m-sm-n1 {\n margin: -0.25rem !important;\n }\n .mt-sm-n1,\n .my-sm-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-sm-n1,\n .mx-sm-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-sm-n1,\n .my-sm-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-sm-n1,\n .mx-sm-n1 {\n margin-left: -0.25rem !important;\n }\n .m-sm-n2 {\n margin: -0.5rem !important;\n }\n .mt-sm-n2,\n .my-sm-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-sm-n2,\n .mx-sm-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-sm-n2,\n .my-sm-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-sm-n2,\n .mx-sm-n2 {\n margin-left: -0.5rem !important;\n }\n .m-sm-n3 {\n margin: -1rem !important;\n }\n .mt-sm-n3,\n .my-sm-n3 {\n margin-top: -1rem !important;\n }\n .mr-sm-n3,\n .mx-sm-n3 {\n margin-right: -1rem !important;\n }\n .mb-sm-n3,\n .my-sm-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-sm-n3,\n .mx-sm-n3 {\n margin-left: -1rem !important;\n }\n .m-sm-n4 {\n margin: -1.5rem !important;\n }\n .mt-sm-n4,\n .my-sm-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-sm-n4,\n .mx-sm-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-sm-n4,\n .my-sm-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-sm-n4,\n .mx-sm-n4 {\n margin-left: -1.5rem !important;\n }\n .m-sm-n5 {\n margin: -3rem !important;\n }\n .mt-sm-n5,\n .my-sm-n5 {\n margin-top: -3rem !important;\n }\n .mr-sm-n5,\n .mx-sm-n5 {\n margin-right: -3rem !important;\n }\n .mb-sm-n5,\n .my-sm-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-sm-n5,\n .mx-sm-n5 {\n margin-left: -3rem !important;\n }\n .m-sm-auto {\n margin: auto !important;\n }\n .mt-sm-auto,\n .my-sm-auto {\n margin-top: auto !important;\n }\n .mr-sm-auto,\n .mx-sm-auto {\n margin-right: auto !important;\n }\n .mb-sm-auto,\n .my-sm-auto {\n margin-bottom: auto !important;\n }\n .ml-sm-auto,\n .mx-sm-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 768px) {\n .m-md-0 {\n margin: 0 !important;\n }\n .mt-md-0,\n .my-md-0 {\n margin-top: 0 !important;\n }\n .mr-md-0,\n .mx-md-0 {\n margin-right: 0 !important;\n }\n .mb-md-0,\n .my-md-0 {\n margin-bottom: 0 !important;\n }\n .ml-md-0,\n .mx-md-0 {\n margin-left: 0 !important;\n }\n .m-md-1 {\n margin: 0.25rem !important;\n }\n .mt-md-1,\n .my-md-1 {\n margin-top: 0.25rem !important;\n }\n .mr-md-1,\n .mx-md-1 {\n margin-right: 0.25rem !important;\n }\n .mb-md-1,\n .my-md-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-md-1,\n .mx-md-1 {\n margin-left: 0.25rem !important;\n }\n .m-md-2 {\n margin: 0.5rem !important;\n }\n .mt-md-2,\n .my-md-2 {\n margin-top: 0.5rem !important;\n }\n .mr-md-2,\n .mx-md-2 {\n margin-right: 0.5rem !important;\n }\n .mb-md-2,\n .my-md-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-md-2,\n .mx-md-2 {\n margin-left: 0.5rem !important;\n }\n .m-md-3 {\n margin: 1rem !important;\n }\n .mt-md-3,\n .my-md-3 {\n margin-top: 1rem !important;\n }\n .mr-md-3,\n .mx-md-3 {\n margin-right: 1rem !important;\n }\n .mb-md-3,\n .my-md-3 {\n margin-bottom: 1rem !important;\n }\n .ml-md-3,\n .mx-md-3 {\n margin-left: 1rem !important;\n }\n .m-md-4 {\n margin: 1.5rem !important;\n }\n .mt-md-4,\n .my-md-4 {\n margin-top: 1.5rem !important;\n }\n .mr-md-4,\n .mx-md-4 {\n margin-right: 1.5rem !important;\n }\n .mb-md-4,\n .my-md-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-md-4,\n .mx-md-4 {\n margin-left: 1.5rem !important;\n }\n .m-md-5 {\n margin: 3rem !important;\n }\n .mt-md-5,\n .my-md-5 {\n margin-top: 3rem !important;\n }\n .mr-md-5,\n .mx-md-5 {\n margin-right: 3rem !important;\n }\n .mb-md-5,\n .my-md-5 {\n margin-bottom: 3rem !important;\n }\n .ml-md-5,\n .mx-md-5 {\n margin-left: 3rem !important;\n }\n .p-md-0 {\n padding: 0 !important;\n }\n .pt-md-0,\n .py-md-0 {\n padding-top: 0 !important;\n }\n .pr-md-0,\n .px-md-0 {\n padding-right: 0 !important;\n }\n .pb-md-0,\n .py-md-0 {\n padding-bottom: 0 !important;\n }\n .pl-md-0,\n .px-md-0 {\n padding-left: 0 !important;\n }\n .p-md-1 {\n padding: 0.25rem !important;\n }\n .pt-md-1,\n .py-md-1 {\n padding-top: 0.25rem !important;\n }\n .pr-md-1,\n .px-md-1 {\n padding-right: 0.25rem !important;\n }\n .pb-md-1,\n .py-md-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-md-1,\n .px-md-1 {\n padding-left: 0.25rem !important;\n }\n .p-md-2 {\n padding: 0.5rem !important;\n }\n .pt-md-2,\n .py-md-2 {\n padding-top: 0.5rem !important;\n }\n .pr-md-2,\n .px-md-2 {\n padding-right: 0.5rem !important;\n }\n .pb-md-2,\n .py-md-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-md-2,\n .px-md-2 {\n padding-left: 0.5rem !important;\n }\n .p-md-3 {\n padding: 1rem !important;\n }\n .pt-md-3,\n .py-md-3 {\n padding-top: 1rem !important;\n }\n .pr-md-3,\n .px-md-3 {\n padding-right: 1rem !important;\n }\n .pb-md-3,\n .py-md-3 {\n padding-bottom: 1rem !important;\n }\n .pl-md-3,\n .px-md-3 {\n padding-left: 1rem !important;\n }\n .p-md-4 {\n padding: 1.5rem !important;\n }\n .pt-md-4,\n .py-md-4 {\n padding-top: 1.5rem !important;\n }\n .pr-md-4,\n .px-md-4 {\n padding-right: 1.5rem !important;\n }\n .pb-md-4,\n .py-md-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-md-4,\n .px-md-4 {\n padding-left: 1.5rem !important;\n }\n .p-md-5 {\n padding: 3rem !important;\n }\n .pt-md-5,\n .py-md-5 {\n padding-top: 3rem !important;\n }\n .pr-md-5,\n .px-md-5 {\n padding-right: 3rem !important;\n }\n .pb-md-5,\n .py-md-5 {\n padding-bottom: 3rem !important;\n }\n .pl-md-5,\n .px-md-5 {\n padding-left: 3rem !important;\n }\n .m-md-n1 {\n margin: -0.25rem !important;\n }\n .mt-md-n1,\n .my-md-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-md-n1,\n .mx-md-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-md-n1,\n .my-md-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-md-n1,\n .mx-md-n1 {\n margin-left: -0.25rem !important;\n }\n .m-md-n2 {\n margin: -0.5rem !important;\n }\n .mt-md-n2,\n .my-md-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-md-n2,\n .mx-md-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-md-n2,\n .my-md-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-md-n2,\n .mx-md-n2 {\n margin-left: -0.5rem !important;\n }\n .m-md-n3 {\n margin: -1rem !important;\n }\n .mt-md-n3,\n .my-md-n3 {\n margin-top: -1rem !important;\n }\n .mr-md-n3,\n .mx-md-n3 {\n margin-right: -1rem !important;\n }\n .mb-md-n3,\n .my-md-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-md-n3,\n .mx-md-n3 {\n margin-left: -1rem !important;\n }\n .m-md-n4 {\n margin: -1.5rem !important;\n }\n .mt-md-n4,\n .my-md-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-md-n4,\n .mx-md-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-md-n4,\n .my-md-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-md-n4,\n .mx-md-n4 {\n margin-left: -1.5rem !important;\n }\n .m-md-n5 {\n margin: -3rem !important;\n }\n .mt-md-n5,\n .my-md-n5 {\n margin-top: -3rem !important;\n }\n .mr-md-n5,\n .mx-md-n5 {\n margin-right: -3rem !important;\n }\n .mb-md-n5,\n .my-md-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-md-n5,\n .mx-md-n5 {\n margin-left: -3rem !important;\n }\n .m-md-auto {\n margin: auto !important;\n }\n .mt-md-auto,\n .my-md-auto {\n margin-top: auto !important;\n }\n .mr-md-auto,\n .mx-md-auto {\n margin-right: auto !important;\n }\n .mb-md-auto,\n .my-md-auto {\n margin-bottom: auto !important;\n }\n .ml-md-auto,\n .mx-md-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 992px) {\n .m-lg-0 {\n margin: 0 !important;\n }\n .mt-lg-0,\n .my-lg-0 {\n margin-top: 0 !important;\n }\n .mr-lg-0,\n .mx-lg-0 {\n margin-right: 0 !important;\n }\n .mb-lg-0,\n .my-lg-0 {\n margin-bottom: 0 !important;\n }\n .ml-lg-0,\n .mx-lg-0 {\n margin-left: 0 !important;\n }\n .m-lg-1 {\n margin: 0.25rem !important;\n }\n .mt-lg-1,\n .my-lg-1 {\n margin-top: 0.25rem !important;\n }\n .mr-lg-1,\n .mx-lg-1 {\n margin-right: 0.25rem !important;\n }\n .mb-lg-1,\n .my-lg-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-lg-1,\n .mx-lg-1 {\n margin-left: 0.25rem !important;\n }\n .m-lg-2 {\n margin: 0.5rem !important;\n }\n .mt-lg-2,\n .my-lg-2 {\n margin-top: 0.5rem !important;\n }\n .mr-lg-2,\n .mx-lg-2 {\n margin-right: 0.5rem !important;\n }\n .mb-lg-2,\n .my-lg-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-lg-2,\n .mx-lg-2 {\n margin-left: 0.5rem !important;\n }\n .m-lg-3 {\n margin: 1rem !important;\n }\n .mt-lg-3,\n .my-lg-3 {\n margin-top: 1rem !important;\n }\n .mr-lg-3,\n .mx-lg-3 {\n margin-right: 1rem !important;\n }\n .mb-lg-3,\n .my-lg-3 {\n margin-bottom: 1rem !important;\n }\n .ml-lg-3,\n .mx-lg-3 {\n margin-left: 1rem !important;\n }\n .m-lg-4 {\n margin: 1.5rem !important;\n }\n .mt-lg-4,\n .my-lg-4 {\n margin-top: 1.5rem !important;\n }\n .mr-lg-4,\n .mx-lg-4 {\n margin-right: 1.5rem !important;\n }\n .mb-lg-4,\n .my-lg-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-lg-4,\n .mx-lg-4 {\n margin-left: 1.5rem !important;\n }\n .m-lg-5 {\n margin: 3rem !important;\n }\n .mt-lg-5,\n .my-lg-5 {\n margin-top: 3rem !important;\n }\n .mr-lg-5,\n .mx-lg-5 {\n margin-right: 3rem !important;\n }\n .mb-lg-5,\n .my-lg-5 {\n margin-bottom: 3rem !important;\n }\n .ml-lg-5,\n .mx-lg-5 {\n margin-left: 3rem !important;\n }\n .p-lg-0 {\n padding: 0 !important;\n }\n .pt-lg-0,\n .py-lg-0 {\n padding-top: 0 !important;\n }\n .pr-lg-0,\n .px-lg-0 {\n padding-right: 0 !important;\n }\n .pb-lg-0,\n .py-lg-0 {\n padding-bottom: 0 !important;\n }\n .pl-lg-0,\n .px-lg-0 {\n padding-left: 0 !important;\n }\n .p-lg-1 {\n padding: 0.25rem !important;\n }\n .pt-lg-1,\n .py-lg-1 {\n padding-top: 0.25rem !important;\n }\n .pr-lg-1,\n .px-lg-1 {\n padding-right: 0.25rem !important;\n }\n .pb-lg-1,\n .py-lg-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-lg-1,\n .px-lg-1 {\n padding-left: 0.25rem !important;\n }\n .p-lg-2 {\n padding: 0.5rem !important;\n }\n .pt-lg-2,\n .py-lg-2 {\n padding-top: 0.5rem !important;\n }\n .pr-lg-2,\n .px-lg-2 {\n padding-right: 0.5rem !important;\n }\n .pb-lg-2,\n .py-lg-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-lg-2,\n .px-lg-2 {\n padding-left: 0.5rem !important;\n }\n .p-lg-3 {\n padding: 1rem !important;\n }\n .pt-lg-3,\n .py-lg-3 {\n padding-top: 1rem !important;\n }\n .pr-lg-3,\n .px-lg-3 {\n padding-right: 1rem !important;\n }\n .pb-lg-3,\n .py-lg-3 {\n padding-bottom: 1rem !important;\n }\n .pl-lg-3,\n .px-lg-3 {\n padding-left: 1rem !important;\n }\n .p-lg-4 {\n padding: 1.5rem !important;\n }\n .pt-lg-4,\n .py-lg-4 {\n padding-top: 1.5rem !important;\n }\n .pr-lg-4,\n .px-lg-4 {\n padding-right: 1.5rem !important;\n }\n .pb-lg-4,\n .py-lg-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-lg-4,\n .px-lg-4 {\n padding-left: 1.5rem !important;\n }\n .p-lg-5 {\n padding: 3rem !important;\n }\n .pt-lg-5,\n .py-lg-5 {\n padding-top: 3rem !important;\n }\n .pr-lg-5,\n .px-lg-5 {\n padding-right: 3rem !important;\n }\n .pb-lg-5,\n .py-lg-5 {\n padding-bottom: 3rem !important;\n }\n .pl-lg-5,\n .px-lg-5 {\n padding-left: 3rem !important;\n }\n .m-lg-n1 {\n margin: -0.25rem !important;\n }\n .mt-lg-n1,\n .my-lg-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-lg-n1,\n .mx-lg-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-lg-n1,\n .my-lg-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-lg-n1,\n .mx-lg-n1 {\n margin-left: -0.25rem !important;\n }\n .m-lg-n2 {\n margin: -0.5rem !important;\n }\n .mt-lg-n2,\n .my-lg-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-lg-n2,\n .mx-lg-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-lg-n2,\n .my-lg-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-lg-n2,\n .mx-lg-n2 {\n margin-left: -0.5rem !important;\n }\n .m-lg-n3 {\n margin: -1rem !important;\n }\n .mt-lg-n3,\n .my-lg-n3 {\n margin-top: -1rem !important;\n }\n .mr-lg-n3,\n .mx-lg-n3 {\n margin-right: -1rem !important;\n }\n .mb-lg-n3,\n .my-lg-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-lg-n3,\n .mx-lg-n3 {\n margin-left: -1rem !important;\n }\n .m-lg-n4 {\n margin: -1.5rem !important;\n }\n .mt-lg-n4,\n .my-lg-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-lg-n4,\n .mx-lg-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-lg-n4,\n .my-lg-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-lg-n4,\n .mx-lg-n4 {\n margin-left: -1.5rem !important;\n }\n .m-lg-n5 {\n margin: -3rem !important;\n }\n .mt-lg-n5,\n .my-lg-n5 {\n margin-top: -3rem !important;\n }\n .mr-lg-n5,\n .mx-lg-n5 {\n margin-right: -3rem !important;\n }\n .mb-lg-n5,\n .my-lg-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-lg-n5,\n .mx-lg-n5 {\n margin-left: -3rem !important;\n }\n .m-lg-auto {\n margin: auto !important;\n }\n .mt-lg-auto,\n .my-lg-auto {\n margin-top: auto !important;\n }\n .mr-lg-auto,\n .mx-lg-auto {\n margin-right: auto !important;\n }\n .mb-lg-auto,\n .my-lg-auto {\n margin-bottom: auto !important;\n }\n .ml-lg-auto,\n .mx-lg-auto {\n margin-left: auto !important;\n }\n}\n\n@media (min-width: 1200px) {\n .m-xl-0 {\n margin: 0 !important;\n }\n .mt-xl-0,\n .my-xl-0 {\n margin-top: 0 !important;\n }\n .mr-xl-0,\n .mx-xl-0 {\n margin-right: 0 !important;\n }\n .mb-xl-0,\n .my-xl-0 {\n margin-bottom: 0 !important;\n }\n .ml-xl-0,\n .mx-xl-0 {\n margin-left: 0 !important;\n }\n .m-xl-1 {\n margin: 0.25rem !important;\n }\n .mt-xl-1,\n .my-xl-1 {\n margin-top: 0.25rem !important;\n }\n .mr-xl-1,\n .mx-xl-1 {\n margin-right: 0.25rem !important;\n }\n .mb-xl-1,\n .my-xl-1 {\n margin-bottom: 0.25rem !important;\n }\n .ml-xl-1,\n .mx-xl-1 {\n margin-left: 0.25rem !important;\n }\n .m-xl-2 {\n margin: 0.5rem !important;\n }\n .mt-xl-2,\n .my-xl-2 {\n margin-top: 0.5rem !important;\n }\n .mr-xl-2,\n .mx-xl-2 {\n margin-right: 0.5rem !important;\n }\n .mb-xl-2,\n .my-xl-2 {\n margin-bottom: 0.5rem !important;\n }\n .ml-xl-2,\n .mx-xl-2 {\n margin-left: 0.5rem !important;\n }\n .m-xl-3 {\n margin: 1rem !important;\n }\n .mt-xl-3,\n .my-xl-3 {\n margin-top: 1rem !important;\n }\n .mr-xl-3,\n .mx-xl-3 {\n margin-right: 1rem !important;\n }\n .mb-xl-3,\n .my-xl-3 {\n margin-bottom: 1rem !important;\n }\n .ml-xl-3,\n .mx-xl-3 {\n margin-left: 1rem !important;\n }\n .m-xl-4 {\n margin: 1.5rem !important;\n }\n .mt-xl-4,\n .my-xl-4 {\n margin-top: 1.5rem !important;\n }\n .mr-xl-4,\n .mx-xl-4 {\n margin-right: 1.5rem !important;\n }\n .mb-xl-4,\n .my-xl-4 {\n margin-bottom: 1.5rem !important;\n }\n .ml-xl-4,\n .mx-xl-4 {\n margin-left: 1.5rem !important;\n }\n .m-xl-5 {\n margin: 3rem !important;\n }\n .mt-xl-5,\n .my-xl-5 {\n margin-top: 3rem !important;\n }\n .mr-xl-5,\n .mx-xl-5 {\n margin-right: 3rem !important;\n }\n .mb-xl-5,\n .my-xl-5 {\n margin-bottom: 3rem !important;\n }\n .ml-xl-5,\n .mx-xl-5 {\n margin-left: 3rem !important;\n }\n .p-xl-0 {\n padding: 0 !important;\n }\n .pt-xl-0,\n .py-xl-0 {\n padding-top: 0 !important;\n }\n .pr-xl-0,\n .px-xl-0 {\n padding-right: 0 !important;\n }\n .pb-xl-0,\n .py-xl-0 {\n padding-bottom: 0 !important;\n }\n .pl-xl-0,\n .px-xl-0 {\n padding-left: 0 !important;\n }\n .p-xl-1 {\n padding: 0.25rem !important;\n }\n .pt-xl-1,\n .py-xl-1 {\n padding-top: 0.25rem !important;\n }\n .pr-xl-1,\n .px-xl-1 {\n padding-right: 0.25rem !important;\n }\n .pb-xl-1,\n .py-xl-1 {\n padding-bottom: 0.25rem !important;\n }\n .pl-xl-1,\n .px-xl-1 {\n padding-left: 0.25rem !important;\n }\n .p-xl-2 {\n padding: 0.5rem !important;\n }\n .pt-xl-2,\n .py-xl-2 {\n padding-top: 0.5rem !important;\n }\n .pr-xl-2,\n .px-xl-2 {\n padding-right: 0.5rem !important;\n }\n .pb-xl-2,\n .py-xl-2 {\n padding-bottom: 0.5rem !important;\n }\n .pl-xl-2,\n .px-xl-2 {\n padding-left: 0.5rem !important;\n }\n .p-xl-3 {\n padding: 1rem !important;\n }\n .pt-xl-3,\n .py-xl-3 {\n padding-top: 1rem !important;\n }\n .pr-xl-3,\n .px-xl-3 {\n padding-right: 1rem !important;\n }\n .pb-xl-3,\n .py-xl-3 {\n padding-bottom: 1rem !important;\n }\n .pl-xl-3,\n .px-xl-3 {\n padding-left: 1rem !important;\n }\n .p-xl-4 {\n padding: 1.5rem !important;\n }\n .pt-xl-4,\n .py-xl-4 {\n padding-top: 1.5rem !important;\n }\n .pr-xl-4,\n .px-xl-4 {\n padding-right: 1.5rem !important;\n }\n .pb-xl-4,\n .py-xl-4 {\n padding-bottom: 1.5rem !important;\n }\n .pl-xl-4,\n .px-xl-4 {\n padding-left: 1.5rem !important;\n }\n .p-xl-5 {\n padding: 3rem !important;\n }\n .pt-xl-5,\n .py-xl-5 {\n padding-top: 3rem !important;\n }\n .pr-xl-5,\n .px-xl-5 {\n padding-right: 3rem !important;\n }\n .pb-xl-5,\n .py-xl-5 {\n padding-bottom: 3rem !important;\n }\n .pl-xl-5,\n .px-xl-5 {\n padding-left: 3rem !important;\n }\n .m-xl-n1 {\n margin: -0.25rem !important;\n }\n .mt-xl-n1,\n .my-xl-n1 {\n margin-top: -0.25rem !important;\n }\n .mr-xl-n1,\n .mx-xl-n1 {\n margin-right: -0.25rem !important;\n }\n .mb-xl-n1,\n .my-xl-n1 {\n margin-bottom: -0.25rem !important;\n }\n .ml-xl-n1,\n .mx-xl-n1 {\n margin-left: -0.25rem !important;\n }\n .m-xl-n2 {\n margin: -0.5rem !important;\n }\n .mt-xl-n2,\n .my-xl-n2 {\n margin-top: -0.5rem !important;\n }\n .mr-xl-n2,\n .mx-xl-n2 {\n margin-right: -0.5rem !important;\n }\n .mb-xl-n2,\n .my-xl-n2 {\n margin-bottom: -0.5rem !important;\n }\n .ml-xl-n2,\n .mx-xl-n2 {\n margin-left: -0.5rem !important;\n }\n .m-xl-n3 {\n margin: -1rem !important;\n }\n .mt-xl-n3,\n .my-xl-n3 {\n margin-top: -1rem !important;\n }\n .mr-xl-n3,\n .mx-xl-n3 {\n margin-right: -1rem !important;\n }\n .mb-xl-n3,\n .my-xl-n3 {\n margin-bottom: -1rem !important;\n }\n .ml-xl-n3,\n .mx-xl-n3 {\n margin-left: -1rem !important;\n }\n .m-xl-n4 {\n margin: -1.5rem !important;\n }\n .mt-xl-n4,\n .my-xl-n4 {\n margin-top: -1.5rem !important;\n }\n .mr-xl-n4,\n .mx-xl-n4 {\n margin-right: -1.5rem !important;\n }\n .mb-xl-n4,\n .my-xl-n4 {\n margin-bottom: -1.5rem !important;\n }\n .ml-xl-n4,\n .mx-xl-n4 {\n margin-left: -1.5rem !important;\n }\n .m-xl-n5 {\n margin: -3rem !important;\n }\n .mt-xl-n5,\n .my-xl-n5 {\n margin-top: -3rem !important;\n }\n .mr-xl-n5,\n .mx-xl-n5 {\n margin-right: -3rem !important;\n }\n .mb-xl-n5,\n .my-xl-n5 {\n margin-bottom: -3rem !important;\n }\n .ml-xl-n5,\n .mx-xl-n5 {\n margin-left: -3rem !important;\n }\n .m-xl-auto {\n margin: auto !important;\n }\n .mt-xl-auto,\n .my-xl-auto {\n margin-top: auto !important;\n }\n .mr-xl-auto,\n .mx-xl-auto {\n margin-right: auto !important;\n }\n .mb-xl-auto,\n .my-xl-auto {\n margin-bottom: auto !important;\n }\n .ml-xl-auto,\n .mx-xl-auto {\n margin-left: auto !important;\n }\n}\n\n.stretched-link::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n pointer-events: auto;\n content: \"\";\n background-color: rgba(0, 0, 0, 0);\n}\n\n.text-monospace {\n font-family: SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace !important;\n}\n\n.text-justify {\n text-align: justify !important;\n}\n\n.text-wrap {\n white-space: normal !important;\n}\n\n.text-nowrap {\n white-space: nowrap !important;\n}\n\n.text-truncate {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.text-left {\n text-align: left !important;\n}\n\n.text-right {\n text-align: right !important;\n}\n\n.text-center {\n text-align: center !important;\n}\n\n@media (min-width: 576px) {\n .text-sm-left {\n text-align: left !important;\n }\n .text-sm-right {\n text-align: right !important;\n }\n .text-sm-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 768px) {\n .text-md-left {\n text-align: left !important;\n }\n .text-md-right {\n text-align: right !important;\n }\n .text-md-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 992px) {\n .text-lg-left {\n text-align: left !important;\n }\n .text-lg-right {\n text-align: right !important;\n }\n .text-lg-center {\n text-align: center !important;\n }\n}\n\n@media (min-width: 1200px) {\n .text-xl-left {\n text-align: left !important;\n }\n .text-xl-right {\n text-align: right !important;\n }\n .text-xl-center {\n text-align: center !important;\n }\n}\n\n.text-lowercase {\n text-transform: lowercase !important;\n}\n\n.text-uppercase {\n text-transform: uppercase !important;\n}\n\n.text-capitalize {\n text-transform: capitalize !important;\n}\n\n.font-weight-light {\n font-weight: 300 !important;\n}\n\n.font-weight-lighter {\n font-weight: lighter !important;\n}\n\n.font-weight-normal {\n font-weight: 400 !important;\n}\n\n.font-weight-bold {\n font-weight: 700 !important;\n}\n\n.font-weight-bolder {\n font-weight: bolder !important;\n}\n\n.font-italic {\n font-style: italic !important;\n}\n\n.text-white {\n color: #fff !important;\n}\n\n.text-primary {\n color: #007bff !important;\n}\n\na.text-primary:hover, a.text-primary:focus {\n color: #0056b3 !important;\n}\n\n.text-secondary {\n color: #6c757d !important;\n}\n\na.text-secondary:hover, a.text-secondary:focus {\n color: #494f54 !important;\n}\n\n.text-success {\n color: #28a745 !important;\n}\n\na.text-success:hover, a.text-success:focus {\n color: #19692c !important;\n}\n\n.text-info {\n color: #17a2b8 !important;\n}\n\na.text-info:hover, a.text-info:focus {\n color: #0f6674 !important;\n}\n\n.text-warning {\n color: #ffc107 !important;\n}\n\na.text-warning:hover, a.text-warning:focus {\n color: #ba8b00 !important;\n}\n\n.text-danger {\n color: #dc3545 !important;\n}\n\na.text-danger:hover, a.text-danger:focus {\n color: #a71d2a !important;\n}\n\n.text-light {\n color: #f8f9fa !important;\n}\n\na.text-light:hover, a.text-light:focus {\n color: #cbd3da !important;\n}\n\n.text-dark {\n color: #343a40 !important;\n}\n\na.text-dark:hover, a.text-dark:focus {\n color: #121416 !important;\n}\n\n.text-body {\n color: #212529 !important;\n}\n\n.text-muted {\n color: #6c757d !important;\n}\n\n.text-black-50 {\n color: rgba(0, 0, 0, 0.5) !important;\n}\n\n.text-white-50 {\n color: rgba(255, 255, 255, 0.5) !important;\n}\n\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n.text-decoration-none {\n text-decoration: none !important;\n}\n\n.text-break {\n word-wrap: break-word !important;\n}\n\n.text-reset {\n color: inherit !important;\n}\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n\n@media print {\n *,\n *::before,\n *::after {\n text-shadow: none !important;\n box-shadow: none !important;\n }\n a:not(.btn) {\n text-decoration: underline;\n }\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: 1px solid #adb5bd;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n @page {\n size: a3;\n }\n body {\n min-width: 992px !important;\n }\n .container {\n min-width: 992px !important;\n }\n .navbar {\n display: none;\n }\n .badge {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #dee2e6 !important;\n }\n .table-dark {\n color: inherit;\n }\n .table-dark th,\n .table-dark td,\n .table-dark thead th,\n .table-dark tbody + tbody {\n border-color: #dee2e6;\n }\n .table .thead-dark th {\n color: inherit;\n border-color: #dee2e6;\n }\n}\n\n/*# sourceMappingURL=bootstrap.css.map */","// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Originally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS-an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular pseudo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover() {\n &:hover { @content; }\n}\n\n@mixin hover-focus() {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus() {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active() {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n","// stylelint-disable declaration-no-important, selector-list-comma-newline-after\n\n//\n// Headings\n//\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n margin-bottom: $headings-margin-bottom;\n font-family: $headings-font-family;\n font-weight: $headings-font-weight;\n line-height: $headings-line-height;\n color: $headings-color;\n}\n\nh1, .h1 { @include font-size($h1-font-size); }\nh2, .h2 { @include font-size($h2-font-size); }\nh3, .h3 { @include font-size($h3-font-size); }\nh4, .h4 { @include font-size($h4-font-size); }\nh5, .h5 { @include font-size($h5-font-size); }\nh6, .h6 { @include font-size($h6-font-size); }\n\n.lead {\n @include font-size($lead-font-size);\n font-weight: $lead-font-weight;\n}\n\n// Type display classes\n.display-1 {\n @include font-size($display1-size);\n font-weight: $display1-weight;\n line-height: $display-line-height;\n}\n.display-2 {\n @include font-size($display2-size);\n font-weight: $display2-weight;\n line-height: $display-line-height;\n}\n.display-3 {\n @include font-size($display3-size);\n font-weight: $display3-weight;\n line-height: $display-line-height;\n}\n.display-4 {\n @include font-size($display4-size);\n font-weight: $display4-weight;\n line-height: $display-line-height;\n}\n\n\n//\n// Horizontal rules\n//\n\nhr {\n margin-top: $hr-margin-y;\n margin-bottom: $hr-margin-y;\n border: 0;\n border-top: $hr-border-width solid $hr-border-color;\n}\n\n\n//\n// Emphasis\n//\n\nsmall,\n.small {\n @include font-size($small-font-size);\n font-weight: $font-weight-normal;\n}\n\nmark,\n.mark {\n padding: $mark-padding;\n background-color: $mark-bg;\n}\n\n\n//\n// Lists\n//\n\n.list-unstyled {\n @include list-unstyled();\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n @include list-unstyled();\n}\n.list-inline-item {\n display: inline-block;\n\n &:not(:last-child) {\n margin-right: $list-inline-padding;\n }\n}\n\n\n//\n// Misc\n//\n\n// Builds on `abbr`\n.initialism {\n @include font-size(90%);\n text-transform: uppercase;\n}\n\n// Blockquotes\n.blockquote {\n margin-bottom: $spacer;\n @include font-size($blockquote-font-size);\n}\n\n.blockquote-footer {\n display: block;\n @include font-size($blockquote-small-font-size);\n color: $blockquote-small-color;\n\n &::before {\n content: \"\\2014\\00A0\"; // em dash, nbsp\n }\n}\n","// Lists\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n@mixin list-unstyled() {\n padding-left: 0;\n list-style: none;\n}\n","// Responsive images (ensure images don't scale beyond their parents)\n//\n// This is purposefully opt-in via an explicit class rather than being the default for all `<img>`s.\n// We previously tried the \"images are responsive by default\" approach in Bootstrap v2,\n// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)\n// which weren't expecting the images within themselves to be involuntarily resized.\n// See also https://github.com/twbs/bootstrap/issues/18178\n.img-fluid {\n @include img-fluid();\n}\n\n\n// Image thumbnails\n.img-thumbnail {\n padding: $thumbnail-padding;\n background-color: $thumbnail-bg;\n border: $thumbnail-border-width solid $thumbnail-border-color;\n @include border-radius($thumbnail-border-radius);\n @include box-shadow($thumbnail-box-shadow);\n\n // Keep them at most 100% wide\n @include img-fluid();\n}\n\n//\n// Figures\n//\n\n.figure {\n // Ensures the caption's text aligns with the image.\n display: inline-block;\n}\n\n.figure-img {\n margin-bottom: $spacer / 2;\n line-height: 1;\n}\n\n.figure-caption {\n @include font-size($figure-caption-font-size);\n color: $figure-caption-color;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n\n@mixin img-fluid() {\n // Part 1: Set a maximum relative to the parent\n max-width: 100%;\n // Part 2: Override the height to auto, otherwise images will be stretched\n // when setting a width and height attribute on the img element.\n height: auto;\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size.\n\n@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {\n background-image: url($file-1x);\n\n // Autoprefixer takes care of adding -webkit-min-device-pixel-ratio and -o-min-device-pixel-ratio,\n // but doesn't convert dppx=>dpi.\n // There's no such thing as unprefixed min-device-pixel-ratio since it's nonstandard.\n // Compatibility info: https://caniuse.com/#feat=css-media-resolution\n @media only screen and (min-resolution: 192dpi), // IE9-11 don't support dppx\n only screen and (min-resolution: 2dppx) { // Standardized\n background-image: url($file-2x);\n background-size: $width-1x $height-1x;\n }\n @include deprecate(\"`img-retina()`\", \"v4.3.0\", \"v5\");\n}\n","// stylelint-disable property-blacklist\n// Single side border-radius\n\n// Helper function to replace negative values with 0\n@function valid-radius($radius) {\n $return: ();\n @each $value in $radius {\n @if type-of($value) == number {\n $return: append($return, max($value, 0));\n } @else {\n $return: append($return, $value);\n }\n }\n @return $return;\n}\n\n@mixin border-radius($radius: $border-radius, $fallback-border-radius: false) {\n @if $enable-rounded {\n border-radius: valid-radius($radius);\n }\n @else if $fallback-border-radius != false {\n border-radius: $fallback-border-radius;\n }\n}\n\n@mixin border-top-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n border-top-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: valid-radius($radius);\n border-bottom-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: valid-radius($radius);\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-top-left-radius($radius) {\n @if $enable-rounded {\n border-top-left-radius: valid-radius($radius);\n }\n}\n\n@mixin border-top-right-radius($radius) {\n @if $enable-rounded {\n border-top-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-right-radius($radius) {\n @if $enable-rounded {\n border-bottom-right-radius: valid-radius($radius);\n }\n}\n\n@mixin border-bottom-left-radius($radius) {\n @if $enable-rounded {\n border-bottom-left-radius: valid-radius($radius);\n }\n}\n","// Inline code\ncode {\n @include font-size($code-font-size);\n color: $code-color;\n word-wrap: break-word;\n\n // Streamline the style when inside anchors to avoid broken underline and more\n a > & {\n color: inherit;\n }\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: $kbd-padding-y $kbd-padding-x;\n @include font-size($kbd-font-size);\n color: $kbd-color;\n background-color: $kbd-bg;\n @include border-radius($border-radius-sm);\n @include box-shadow($kbd-box-shadow);\n\n kbd {\n padding: 0;\n @include font-size(100%);\n font-weight: $nested-kbd-font-weight;\n @include box-shadow(none);\n }\n}\n\n// Blocks of code\npre {\n display: block;\n @include font-size($code-font-size);\n color: $pre-color;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n @include font-size(inherit);\n color: inherit;\n word-break: normal;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: $pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n@if $enable-grid-classes {\n // Single container class with breakpoint max-widths\n .container {\n @include make-container();\n @include make-container-max-widths();\n }\n\n // 100% wide container at all breakpoints\n .container-fluid {\n @include make-container();\n }\n\n // Responsive containers that are 100% wide until a breakpoint\n @each $breakpoint, $container-max-width in $container-max-widths {\n .container-#{$breakpoint} {\n @extend .container-fluid;\n }\n\n @include media-breakpoint-up($breakpoint, $grid-breakpoints) {\n %responsive-container-#{$breakpoint} {\n max-width: $container-max-width;\n }\n\n // Extend each breakpoint which is smaller or equal to the current breakpoint\n $extend-breakpoint: true;\n\n @each $name, $width in $grid-breakpoints {\n @if ($extend-breakpoint) {\n .container#{breakpoint-infix($name, $grid-breakpoints)} {\n @extend %responsive-container-#{$breakpoint};\n }\n\n // Once the current breakpoint is reached, stop extending\n @if ($breakpoint == $name) {\n $extend-breakpoint: false;\n }\n }\n }\n }\n }\n}\n\n\n// Row\n//\n// Rows contain your columns.\n\n@if $enable-grid-classes {\n .row {\n @include make-row();\n }\n\n // Remove the negative margin from default .row, then the horizontal padding\n // from all immediate children columns (to prevent runaway style inheritance).\n .no-gutters {\n margin-right: 0;\n margin-left: 0;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: 0;\n padding-left: 0;\n }\n }\n}\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n@if $enable-grid-classes {\n @include make-grid-columns();\n}\n","/// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n@mixin make-container($gutter: $grid-gutter-width) {\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n margin-right: auto;\n margin-left: auto;\n}\n\n\n// For each breakpoint, define the maximum width of the container in a media query\n@mixin make-container-max-widths($max-widths: $container-max-widths, $breakpoints: $grid-breakpoints) {\n @each $breakpoint, $container-max-width in $max-widths {\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n max-width: $container-max-width;\n }\n }\n}\n\n@mixin make-row($gutter: $grid-gutter-width) {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$gutter / 2;\n margin-left: -$gutter / 2;\n}\n\n@mixin make-col-ready($gutter: $grid-gutter-width) {\n position: relative;\n // Prevent columns from becoming too narrow when at smaller grid tiers by\n // always setting `width: 100%;`. This works because we use `flex` values\n // later on to override this initial width.\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n}\n\n@mixin make-col($size, $columns: $grid-columns) {\n flex: 0 0 percentage($size / $columns);\n // Add a `max-width` to ensure content within each column does not blow out\n // the width of the column. Applies to IE10+ and Firefox. Chrome and Safari\n // do not appear to require this.\n max-width: percentage($size / $columns);\n}\n\n@mixin make-col-auto() {\n flex: 0 0 auto;\n width: auto;\n max-width: 100%; // Reset earlier grid tiers\n}\n\n@mixin make-col-offset($size, $columns: $grid-columns) {\n $num: $size / $columns;\n margin-left: if($num == 0, 0, percentage($num));\n}\n\n// Row columns\n//\n// Specify on a parent element(e.g., .row) to force immediate children into NN\n// numberof columns. Supports wrapping to new lines, but does not do a Masonry\n// style grid.\n@mixin row-cols($count) {\n & > * {\n flex: 0 0 100% / $count;\n max-width: 100% / $count;\n }\n}\n","// Breakpoint viewport sizes and media queries.\n//\n// Breakpoints are defined as a map of (name: minimum width), order from small to large:\n//\n// (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)\n//\n// The map defined in the `$grid-breakpoints` global variable is used as the `$breakpoints` argument by default.\n\n// Name of the next breakpoint, or null for the last breakpoint.\n//\n// >> breakpoint-next(sm)\n// md\n// >> breakpoint-next(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// md\n// >> breakpoint-next(sm, $breakpoint-names: (xs sm md lg xl))\n// md\n@function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {\n $n: index($breakpoint-names, $name);\n @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null);\n}\n\n// Minimum breakpoint width. Null for the smallest (first) breakpoint.\n//\n// >> breakpoint-min(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 576px\n@function breakpoint-min($name, $breakpoints: $grid-breakpoints) {\n $min: map-get($breakpoints, $name);\n @return if($min != 0, $min, null);\n}\n\n// Maximum breakpoint width. Null for the largest (last) breakpoint.\n// The maximum value is calculated as the minimum of the next one less 0.02px\n// to work around the limitations of `min-` and `max-` prefixes and viewports with fractional widths.\n// See https://www.w3.org/TR/mediaqueries-4/#mq-min-max\n// Uses 0.02px rather than 0.01px to work around a current rounding bug in Safari.\n// See https://bugs.webkit.org/show_bug.cgi?id=178261\n//\n// >> breakpoint-max(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// 767.98px\n@function breakpoint-max($name, $breakpoints: $grid-breakpoints) {\n $next: breakpoint-next($name, $breakpoints);\n @return if($next, breakpoint-min($next, $breakpoints) - .02, null);\n}\n\n// Returns a blank string if smallest breakpoint, otherwise returns the name with a dash in front.\n// Useful for making responsive utilities.\n//\n// >> breakpoint-infix(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"\" (Returns a blank string)\n// >> breakpoint-infix(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px))\n// \"-sm\"\n@function breakpoint-infix($name, $breakpoints: $grid-breakpoints) {\n @return if(breakpoint-min($name, $breakpoints) == null, \"\", \"-#{$name}\");\n}\n\n// Media of at least the minimum breakpoint width. No query for the smallest breakpoint.\n// Makes the @content apply to the given breakpoint and wider.\n@mixin media-breakpoint-up($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n @if $min {\n @media (min-width: $min) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media of at most the maximum breakpoint width. No query for the largest breakpoint.\n// Makes the @content apply to the given breakpoint and narrower.\n@mixin media-breakpoint-down($name, $breakpoints: $grid-breakpoints) {\n $max: breakpoint-max($name, $breakpoints);\n @if $max {\n @media (max-width: $max) {\n @content;\n }\n } @else {\n @content;\n }\n}\n\n// Media that spans multiple breakpoint widths.\n// Makes the @content apply between the min and max breakpoints\n@mixin media-breakpoint-between($lower, $upper, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($lower, $breakpoints);\n $max: breakpoint-max($upper, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($lower, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($upper, $breakpoints) {\n @content;\n }\n }\n}\n\n// Media between the breakpoint's minimum and maximum widths.\n// No minimum for the smallest breakpoint, and no maximum for the largest one.\n// Makes the @content apply only to the given breakpoint, not viewports any wider or narrower.\n@mixin media-breakpoint-only($name, $breakpoints: $grid-breakpoints) {\n $min: breakpoint-min($name, $breakpoints);\n $max: breakpoint-max($name, $breakpoints);\n\n @if $min != null and $max != null {\n @media (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else if $max == null {\n @include media-breakpoint-up($name, $breakpoints) {\n @content;\n }\n } @else if $min == null {\n @include media-breakpoint-down($name, $breakpoints) {\n @content;\n }\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `$grid-columns`.\n\n@mixin make-grid-columns($columns: $grid-columns, $gutter: $grid-gutter-width, $breakpoints: $grid-breakpoints) {\n // Common properties for all breakpoints\n %grid-column {\n position: relative;\n width: 100%;\n padding-right: $gutter / 2;\n padding-left: $gutter / 2;\n }\n\n @each $breakpoint in map-keys($breakpoints) {\n $infix: breakpoint-infix($breakpoint, $breakpoints);\n\n @if $columns > 0 {\n // Allow columns to stretch full width below their breakpoints\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @extend %grid-column;\n }\n }\n }\n\n .col#{$infix},\n .col#{$infix}-auto {\n @extend %grid-column;\n }\n\n @include media-breakpoint-up($breakpoint, $breakpoints) {\n // Provide basic `.col-{bp}` classes for equal-width flexbox columns\n .col#{$infix} {\n flex-basis: 0;\n flex-grow: 1;\n min-width: 0; // See https://github.com/twbs/bootstrap/issues/25410\n max-width: 100%;\n }\n\n @if $grid-row-columns > 0 {\n @for $i from 1 through $grid-row-columns {\n .row-cols#{$infix}-#{$i} {\n @include row-cols($i);\n }\n }\n }\n\n .col#{$infix}-auto {\n @include make-col-auto();\n }\n\n @if $columns > 0 {\n @for $i from 1 through $columns {\n .col#{$infix}-#{$i} {\n @include make-col($i, $columns);\n }\n }\n }\n\n .order#{$infix}-first { order: -1; }\n\n .order#{$infix}-last { order: $columns + 1; }\n\n @for $i from 0 through $columns {\n .order#{$infix}-#{$i} { order: $i; }\n }\n\n @if $columns > 0 {\n // `$columns - 1` because offsetting by the width of an entire row isn't possible\n @for $i from 0 through ($columns - 1) {\n @if not ($infix == \"\" and $i == 0) { // Avoid emitting useless .offset-0\n .offset#{$infix}-#{$i} {\n @include make-col-offset($i, $columns);\n }\n }\n }\n }\n }\n }\n}\n","//\n// Basic Bootstrap table\n//\n\n.table {\n width: 100%;\n margin-bottom: $spacer;\n color: $table-color;\n background-color: $table-bg; // Reset for nesting within parents with `background-color`.\n\n th,\n td {\n padding: $table-cell-padding;\n vertical-align: top;\n border-top: $table-border-width solid $table-border-color;\n }\n\n thead th {\n vertical-align: bottom;\n border-bottom: (2 * $table-border-width) solid $table-border-color;\n }\n\n tbody + tbody {\n border-top: (2 * $table-border-width) solid $table-border-color;\n }\n}\n\n\n//\n// Condensed table w/ half padding\n//\n\n.table-sm {\n th,\n td {\n padding: $table-cell-padding-sm;\n }\n}\n\n\n// Border versions\n//\n// Add or remove borders all around the table and between all the columns.\n\n.table-bordered {\n border: $table-border-width solid $table-border-color;\n\n th,\n td {\n border: $table-border-width solid $table-border-color;\n }\n\n thead {\n th,\n td {\n border-bottom-width: 2 * $table-border-width;\n }\n }\n}\n\n.table-borderless {\n th,\n td,\n thead th,\n tbody + tbody {\n border: 0;\n }\n}\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-accent-bg;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n tbody tr {\n @include hover() {\n color: $table-hover-color;\n background-color: $table-hover-bg;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n@each $color, $value in $theme-colors {\n @include table-row-variant($color, theme-color-level($color, $table-bg-level), theme-color-level($color, $table-border-level));\n}\n\n@include table-row-variant(active, $table-active-bg);\n\n\n// Dark styles\n//\n// Same table markup, but inverted color scheme: dark background and light text.\n\n// stylelint-disable-next-line no-duplicate-selectors\n.table {\n .thead-dark {\n th {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n border-color: $table-dark-border-color;\n }\n }\n\n .thead-light {\n th {\n color: $table-head-color;\n background-color: $table-head-bg;\n border-color: $table-border-color;\n }\n }\n}\n\n.table-dark {\n color: $table-dark-color;\n background-color: $table-dark-bg;\n\n th,\n td,\n thead th {\n border-color: $table-dark-border-color;\n }\n\n &.table-bordered {\n border: 0;\n }\n\n &.table-striped {\n tbody tr:nth-of-type(#{$table-striped-order}) {\n background-color: $table-dark-accent-bg;\n }\n }\n\n &.table-hover {\n tbody tr {\n @include hover() {\n color: $table-dark-hover-color;\n background-color: $table-dark-hover-bg;\n }\n }\n }\n}\n\n\n// Responsive tables\n//\n// Generate series of `.table-responsive-*` classes for configuring the screen\n// size of where your table will overflow.\n\n.table-responsive {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n display: block;\n width: 100%;\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n\n // Prevent double border on horizontal scroll due to use of `display: block;`\n > .table-bordered {\n border: 0;\n }\n }\n }\n }\n}\n","// Tables\n\n@mixin table-row-variant($state, $background, $border: null) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table-#{$state} {\n &,\n > th,\n > td {\n background-color: $background;\n }\n\n @if $border != null {\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $border;\n }\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover {\n $hover-background: darken($background, 5%);\n\n .table-#{$state} {\n @include hover() {\n background-color: $hover-background;\n\n > td,\n > th {\n background-color: $hover-background;\n }\n }\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Textual form controls\n//\n\n.form-control {\n display: block;\n width: 100%;\n height: $input-height;\n padding: $input-padding-y $input-padding-x;\n font-family: $input-font-family;\n @include font-size($input-font-size);\n font-weight: $input-font-weight;\n line-height: $input-line-height;\n color: $input-color;\n background-color: $input-bg;\n background-clip: padding-box;\n border: $input-border-width solid $input-border-color;\n\n // Note: This has no effect on <select>s in some browsers, due to the limited stylability of `<select>`s in CSS.\n @include border-radius($input-border-radius, 0);\n\n @include box-shadow($input-box-shadow);\n @include transition($input-transition);\n\n // Unstyle the caret on `<select>`s in IE10+.\n &::-ms-expand {\n background-color: transparent;\n border: 0;\n }\n\n // Remove select outline from select box in FF\n &:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 $input-color;\n }\n\n // Customize the `:focus` state to imitate native WebKit styles.\n @include form-control-focus($ignore-warning: true);\n\n // Placeholder\n &::placeholder {\n color: $input-placeholder-color;\n // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526.\n opacity: 1;\n }\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &:disabled,\n &[readonly] {\n background-color: $input-disabled-bg;\n // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655.\n opacity: 1;\n }\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n &.form-control {\n appearance: none; // Fix appearance for date inputs in Safari\n }\n}\n\nselect.form-control {\n &:focus::-ms-value {\n // Suppress the nested default white text on blue background highlight given to\n // the selected option text when the (still closed) <select> receives focus\n // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to\n // match the appearance of the native widget.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n}\n\n// Make file inputs better match text inputs by forcing them to new lines.\n.form-control-file,\n.form-control-range {\n display: block;\n width: 100%;\n}\n\n\n//\n// Labels\n//\n\n// For use with horizontal and inline forms, when you need the label (or legend)\n// text to align with the form controls.\n.col-form-label {\n padding-top: add($input-padding-y, $input-border-width);\n padding-bottom: add($input-padding-y, $input-border-width);\n margin-bottom: 0; // Override the `<label>/<legend>` default\n @include font-size(inherit); // Override the `<legend>` default\n line-height: $input-line-height;\n}\n\n.col-form-label-lg {\n padding-top: add($input-padding-y-lg, $input-border-width);\n padding-bottom: add($input-padding-y-lg, $input-border-width);\n @include font-size($input-font-size-lg);\n line-height: $input-line-height-lg;\n}\n\n.col-form-label-sm {\n padding-top: add($input-padding-y-sm, $input-border-width);\n padding-bottom: add($input-padding-y-sm, $input-border-width);\n @include font-size($input-font-size-sm);\n line-height: $input-line-height-sm;\n}\n\n\n// Readonly controls as plain text\n//\n// Apply class to a readonly input to make it appear like regular plain\n// text (without any border, background color, focus indicator)\n\n.form-control-plaintext {\n display: block;\n width: 100%;\n padding: $input-padding-y 0;\n margin-bottom: 0; // match inputs if this class comes on inputs with default margins\n @include font-size($input-font-size);\n line-height: $input-line-height;\n color: $input-plaintext-color;\n background-color: transparent;\n border: solid transparent;\n border-width: $input-border-width 0;\n\n &.form-control-sm,\n &.form-control-lg {\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n// Form control sizing\n//\n// Build on `.form-control` with modifier classes to decrease or increase the\n// height and font-size of form controls.\n//\n// Repeated in `_input_group.scss` to avoid Sass extend issues.\n\n.form-control-sm {\n height: $input-height-sm;\n padding: $input-padding-y-sm $input-padding-x-sm;\n @include font-size($input-font-size-sm);\n line-height: $input-line-height-sm;\n @include border-radius($input-border-radius-sm);\n}\n\n.form-control-lg {\n height: $input-height-lg;\n padding: $input-padding-y-lg $input-padding-x-lg;\n @include font-size($input-font-size-lg);\n line-height: $input-line-height-lg;\n @include border-radius($input-border-radius-lg);\n}\n\n// stylelint-disable-next-line no-duplicate-selectors\nselect.form-control {\n &[size],\n &[multiple] {\n height: auto;\n }\n}\n\ntextarea.form-control {\n height: auto;\n}\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n margin-bottom: $form-group-margin-bottom;\n}\n\n.form-text {\n display: block;\n margin-top: $form-text-margin-top;\n}\n\n\n// Form grid\n//\n// Special replacement for our grid system's `.row` for tighter form layouts.\n\n.form-row {\n display: flex;\n flex-wrap: wrap;\n margin-right: -$form-grid-gutter-width / 2;\n margin-left: -$form-grid-gutter-width / 2;\n\n > .col,\n > [class*=\"col-\"] {\n padding-right: $form-grid-gutter-width / 2;\n padding-left: $form-grid-gutter-width / 2;\n }\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.form-check {\n position: relative;\n display: block;\n padding-left: $form-check-input-gutter;\n}\n\n.form-check-input {\n position: absolute;\n margin-top: $form-check-input-margin-y;\n margin-left: -$form-check-input-gutter;\n\n // Use [disabled] and :disabled for workaround https://github.com/twbs/bootstrap/issues/28247\n &[disabled] ~ .form-check-label,\n &:disabled ~ .form-check-label {\n color: $text-muted;\n }\n}\n\n.form-check-label {\n margin-bottom: 0; // Override default `<label>` bottom margin\n}\n\n.form-check-inline {\n display: inline-flex;\n align-items: center;\n padding-left: 0; // Override base .form-check\n margin-right: $form-check-inline-margin-x;\n\n // Undo .form-check-input defaults and add some `margin-right`.\n .form-check-input {\n position: static;\n margin-top: 0;\n margin-right: $form-check-inline-input-margin-x;\n margin-left: 0;\n }\n}\n\n\n// Form validation\n//\n// Provide feedback to users when form field values are valid or invalid. Works\n// primarily for client-side validation via scoped `:invalid` and `:valid`\n// pseudo-classes but also includes `.is-invalid` and `.is-valid` classes for\n// server side validation.\n\n@each $state, $data in $form-validation-states {\n @include form-validation-state($state, map-get($data, color), map-get($data, icon));\n}\n\n// Inline forms\n//\n// Make forms appear inline(-block) by adding the `.form-inline` class. Inline\n// forms begin stacked on extra small (mobile) devices and then go inline when\n// viewports reach <768px.\n//\n// Requires wrapping inputs and labels with `.form-group` for proper display of\n// default HTML form controls and our custom form controls (e.g., input groups).\n\n.form-inline {\n display: flex;\n flex-flow: row wrap;\n align-items: center; // Prevent shorter elements from growing to same height as others (e.g., small buttons growing to normal sized button height)\n\n // Because we use flex, the initial sizing of checkboxes is collapsed and\n // doesn't occupy the full-width (which is what we want for xs grid tier),\n // so we force that here.\n .form-check {\n width: 100%;\n }\n\n // Kick in the inline\n @include media-breakpoint-up(sm) {\n label {\n display: flex;\n align-items: center;\n justify-content: center;\n margin-bottom: 0;\n }\n\n // Inline-block all the things for \"inline\"\n .form-group {\n display: flex;\n flex: 0 0 auto;\n flex-flow: row wrap;\n align-items: center;\n margin-bottom: 0;\n }\n\n // Allow folks to *not* use `.form-group`\n .form-control {\n display: inline-block;\n width: auto; // Prevent labels from stacking above inputs in `.form-group`\n vertical-align: middle;\n }\n\n // Make static controls behave like regular ones\n .form-control-plaintext {\n display: inline-block;\n }\n\n .input-group,\n .custom-select {\n width: auto;\n }\n\n // Remove default margin on radios/checkboxes that were used for stacking, and\n // then undo the floating of radios and checkboxes to match.\n .form-check {\n display: flex;\n align-items: center;\n justify-content: center;\n width: auto;\n padding-left: 0;\n }\n .form-check-input {\n position: relative;\n flex-shrink: 0;\n margin-top: 0;\n margin-right: $form-check-input-margin-x;\n margin-left: 0;\n }\n\n .custom-control {\n align-items: center;\n justify-content: center;\n }\n .custom-control-label {\n margin-bottom: 0;\n }\n }\n}\n","// stylelint-disable property-blacklist\n@mixin transition($transition...) {\n @if length($transition) == 0 {\n $transition: $transition-base;\n }\n\n @if length($transition) > 1 {\n @each $value in $transition {\n @if $value == null or $value == none {\n @warn \"The keyword 'none' or 'null' must be used as a single argument.\";\n }\n }\n }\n\n @if $enable-transitions {\n @if nth($transition, 1) != null {\n transition: $transition;\n }\n\n @if $enable-prefers-reduced-motion-media-query and nth($transition, 1) != null and nth($transition, 1) != none {\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n }\n}\n","// Form control focus state\n//\n// Generate a customized focus state and for any input with the specified color,\n// which defaults to the `$input-focus-border-color` variable.\n//\n// We highly encourage you to not customize the default value, but instead use\n// this to tweak colors on an as-needed basis. This aesthetic change is based on\n// WebKit's default styles, but applicable to a wider range of browsers. Its\n// usability and accessibility should be taken into account with any change.\n//\n// Example usage: change the default blue border and shadow to white for better\n// contrast against a dark gray background.\n@mixin form-control-focus($ignore-warning: false) {\n &:focus {\n color: $input-focus-color;\n background-color: $input-focus-bg;\n border-color: $input-focus-border-color;\n outline: 0;\n @if $enable-shadows {\n @include box-shadow($input-box-shadow, $input-focus-box-shadow);\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: $input-focus-box-shadow;\n }\n }\n @include deprecate(\"The `form-control-focus()` mixin\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n\n// This mixin uses an `if()` technique to be compatible with Dart Sass\n// See https://github.com/sass/sass/issues/1873#issuecomment-152293725 for more details\n@mixin form-validation-state-selector($state) {\n @if ($state == \"valid\" or $state == \"invalid\") {\n .was-validated #{if(&, \"&\", \"\")}:#{$state},\n #{if(&, \"&\", \"\")}.is-#{$state} {\n @content;\n }\n } @else {\n #{if(&, \"&\", \"\")}.is-#{$state} {\n @content;\n }\n }\n}\n\n@mixin form-validation-state($state, $color, $icon) {\n .#{$state}-feedback {\n display: none;\n width: 100%;\n margin-top: $form-feedback-margin-top;\n @include font-size($form-feedback-font-size);\n color: $color;\n }\n\n .#{$state}-tooltip {\n position: absolute;\n top: 100%;\n z-index: 5;\n display: none;\n max-width: 100%; // Contain to parent when possible\n padding: $form-feedback-tooltip-padding-y $form-feedback-tooltip-padding-x;\n margin-top: .1rem;\n @include font-size($form-feedback-tooltip-font-size);\n line-height: $form-feedback-tooltip-line-height;\n color: color-yiq($color);\n background-color: rgba($color, $form-feedback-tooltip-opacity);\n @include border-radius($form-feedback-tooltip-border-radius);\n }\n\n @include form-validation-state-selector($state) {\n ~ .#{$state}-feedback,\n ~ .#{$state}-tooltip {\n display: block;\n }\n }\n\n .form-control {\n @include form-validation-state-selector($state) {\n border-color: $color;\n\n @if $enable-validation-icons {\n padding-right: $input-height-inner;\n background-image: escape-svg($icon);\n background-repeat: no-repeat;\n background-position: right $input-height-inner-quarter center;\n background-size: $input-height-inner-half $input-height-inner-half;\n }\n\n &:focus {\n border-color: $color;\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n }\n }\n\n // stylelint-disable-next-line selector-no-qualifying-type\n textarea.form-control {\n @include form-validation-state-selector($state) {\n @if $enable-validation-icons {\n padding-right: $input-height-inner;\n background-position: top $input-height-inner-quarter right $input-height-inner-quarter;\n }\n }\n }\n\n .custom-select {\n @include form-validation-state-selector($state) {\n border-color: $color;\n\n @if $enable-validation-icons {\n padding-right: $custom-select-feedback-icon-padding-right;\n background: $custom-select-background, escape-svg($icon) $custom-select-bg no-repeat $custom-select-feedback-icon-position / $custom-select-feedback-icon-size;\n }\n\n &:focus {\n border-color: $color;\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n }\n }\n\n .form-check-input {\n @include form-validation-state-selector($state) {\n ~ .form-check-label {\n color: $color;\n }\n\n ~ .#{$state}-feedback,\n ~ .#{$state}-tooltip {\n display: block;\n }\n }\n }\n\n .custom-control-input {\n @include form-validation-state-selector($state) {\n ~ .custom-control-label {\n color: $color;\n\n &::before {\n border-color: $color;\n }\n }\n\n &:checked {\n ~ .custom-control-label::before {\n border-color: lighten($color, 10%);\n @include gradient-bg(lighten($color, 10%));\n }\n }\n\n &:focus {\n ~ .custom-control-label::before {\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n\n &:not(:checked) ~ .custom-control-label::before {\n border-color: $color;\n }\n }\n }\n }\n\n // custom file\n .custom-file-input {\n @include form-validation-state-selector($state) {\n ~ .custom-file-label {\n border-color: $color;\n }\n\n &:focus {\n ~ .custom-file-label {\n border-color: $color;\n box-shadow: 0 0 0 $input-focus-width rgba($color, .25);\n }\n }\n }\n }\n}\n","// Gradients\n\n@mixin gradient-bg($color) {\n @if $enable-gradients {\n background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x;\n } @else {\n background-color: $color;\n }\n}\n\n// Horizontal gradient, from left to right\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-x($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {\n background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent);\n background-repeat: repeat-x;\n}\n\n// Vertical gradient, from top to bottom\n//\n// Creates two color stops, start and end, by specifying a color and position for each color stop.\n@mixin gradient-y($start-color: $gray-700, $end-color: $gray-800, $start-percent: 0%, $end-percent: 100%) {\n background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent);\n background-repeat: repeat-x;\n}\n\n@mixin gradient-directional($start-color: $gray-700, $end-color: $gray-800, $deg: 45deg) {\n background-image: linear-gradient($deg, $start-color, $end-color);\n background-repeat: repeat-x;\n}\n@mixin gradient-x-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {\n background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-y-three-colors($start-color: $blue, $mid-color: $purple, $color-stop: 50%, $end-color: $red) {\n background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-radial($inner-color: $gray-700, $outer-color: $gray-800) {\n background-image: radial-gradient(circle, $inner-color, $outer-color);\n background-repeat: no-repeat;\n}\n@mixin gradient-striped($color: rgba($white, .15), $angle: 45deg) {\n background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Base styles\n//\n\n.btn {\n display: inline-block;\n font-family: $btn-font-family;\n font-weight: $btn-font-weight;\n color: $body-color;\n text-align: center;\n text-decoration: if($link-decoration == none, null, none);\n white-space: $btn-white-space;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: $btn-border-width solid transparent;\n @include button-size($btn-padding-y, $btn-padding-x, $btn-font-size, $btn-line-height, $btn-border-radius);\n @include transition($btn-transition);\n\n @include hover() {\n color: $body-color;\n text-decoration: none;\n }\n\n &:focus,\n &.focus {\n outline: 0;\n box-shadow: $btn-focus-box-shadow;\n }\n\n // Disabled comes first so active can properly restyle\n &.disabled,\n &:disabled {\n opacity: $btn-disabled-opacity;\n @include box-shadow(none);\n }\n\n &:not(:disabled):not(.disabled) {\n cursor: if($enable-pointer-cursor-for-buttons, pointer, null);\n\n &:active,\n &.active {\n @include box-shadow($btn-active-box-shadow);\n\n &:focus {\n @include box-shadow($btn-focus-box-shadow, $btn-active-box-shadow);\n }\n }\n }\n}\n\n// Future-proof disabling of clicks on `<a>` elements\na.btn.disabled,\nfieldset:disabled a.btn {\n pointer-events: none;\n}\n\n\n//\n// Alternate buttons\n//\n\n@each $color, $value in $theme-colors {\n .btn-#{$color} {\n @include button-variant($value, $value);\n }\n}\n\n@each $color, $value in $theme-colors {\n .btn-outline-#{$color} {\n @include button-outline-variant($value);\n }\n}\n\n\n//\n// Link buttons\n//\n\n// Make a button look and behave like a link\n.btn-link {\n font-weight: $font-weight-normal;\n color: $link-color;\n text-decoration: $link-decoration;\n\n @include hover() {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n\n &:focus,\n &.focus {\n text-decoration: $link-hover-decoration;\n }\n\n &:disabled,\n &.disabled {\n color: $btn-link-disabled-color;\n pointer-events: none;\n }\n\n // No need for an active state here\n}\n\n\n//\n// Button Sizes\n//\n\n.btn-lg {\n @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-line-height-lg, $btn-border-radius-lg);\n}\n\n.btn-sm {\n @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-line-height-sm, $btn-border-radius-sm);\n}\n\n\n//\n// Block button\n//\n\n.btn-block {\n display: block;\n width: 100%;\n\n // Vertically space out multiple block buttons\n + .btn-block {\n margin-top: $btn-block-spacing-y;\n }\n}\n\n// Specificity overrides\ninput[type=\"submit\"],\ninput[type=\"reset\"],\ninput[type=\"button\"] {\n &.btn-block {\n width: 100%;\n }\n}\n","// Button variants\n//\n// Easily pump out default styles, as well as :hover, :focus, :active,\n// and disabled options for all buttons\n\n@mixin button-variant($background, $border, $hover-background: darken($background, 7.5%), $hover-border: darken($border, 10%), $active-background: darken($background, 10%), $active-border: darken($border, 12.5%)) {\n color: color-yiq($background);\n @include gradient-bg($background);\n border-color: $border;\n @include box-shadow($btn-box-shadow);\n\n @include hover() {\n color: color-yiq($hover-background);\n @include gradient-bg($hover-background);\n border-color: $hover-border;\n }\n\n &:focus,\n &.focus {\n color: color-yiq($hover-background);\n @include gradient-bg($hover-background);\n border-color: $hover-border;\n @if $enable-shadows {\n @include box-shadow($btn-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);\n }\n }\n\n // Disabled comes first so active can properly restyle\n &.disabled,\n &:disabled {\n color: color-yiq($background);\n background-color: $background;\n border-color: $border;\n // Remove CSS gradients if they're enabled\n @if $enable-gradients {\n background-image: none;\n }\n }\n\n &:not(:disabled):not(.disabled):active,\n &:not(:disabled):not(.disabled).active,\n .show > &.dropdown-toggle {\n color: color-yiq($active-background);\n background-color: $active-background;\n @if $enable-gradients {\n background-image: none; // Remove the gradient for the pressed/active state\n }\n border-color: $active-border;\n\n &:focus {\n @if $enable-shadows and $btn-active-box-shadow != none {\n @include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($background), $border, 15%), .5);\n }\n }\n }\n}\n\n@mixin button-outline-variant($color, $color-hover: color-yiq($color), $active-background: $color, $active-border: $color) {\n color: $color;\n border-color: $color;\n\n @include hover() {\n color: $color-hover;\n background-color: $active-background;\n border-color: $active-border;\n }\n\n &:focus,\n &.focus {\n box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);\n }\n\n &.disabled,\n &:disabled {\n color: $color;\n background-color: transparent;\n }\n\n &:not(:disabled):not(.disabled):active,\n &:not(:disabled):not(.disabled).active,\n .show > &.dropdown-toggle {\n color: color-yiq($active-background);\n background-color: $active-background;\n border-color: $active-border;\n\n &:focus {\n @if $enable-shadows and $btn-active-box-shadow != none {\n @include box-shadow($btn-active-box-shadow, 0 0 0 $btn-focus-width rgba($color, .5));\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: 0 0 0 $btn-focus-width rgba($color, .5);\n }\n }\n }\n}\n\n// Button sizes\n@mixin button-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {\n padding: $padding-y $padding-x;\n @include font-size($font-size);\n line-height: $line-height;\n // Manually declare to provide an override to the browser default\n @include border-radius($border-radius, 0);\n}\n",".fade {\n @include transition($transition-fade);\n\n &:not(.show) {\n opacity: 0;\n }\n}\n\n.collapse {\n &:not(.show) {\n display: none;\n }\n}\n\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n @include transition($transition-collapse);\n}\n","// The dropdown wrapper (`<div>`)\n.dropup,\n.dropright,\n.dropdown,\n.dropleft {\n position: relative;\n}\n\n.dropdown-toggle {\n white-space: nowrap;\n\n // Generate the caret automatically\n @include caret();\n}\n\n// The dropdown menu\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: $zindex-dropdown;\n display: none; // none by default, but block on \"open\" of the menu\n float: left;\n min-width: $dropdown-min-width;\n padding: $dropdown-padding-y 0;\n margin: $dropdown-spacer 0 0; // override default ul\n @include font-size($dropdown-font-size);\n color: $dropdown-color;\n text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)\n list-style: none;\n background-color: $dropdown-bg;\n background-clip: padding-box;\n border: $dropdown-border-width solid $dropdown-border-color;\n @include border-radius($dropdown-border-radius);\n @include box-shadow($dropdown-box-shadow);\n}\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .dropdown-menu#{$infix}-left {\n right: auto;\n left: 0;\n }\n\n .dropdown-menu#{$infix}-right {\n right: 0;\n left: auto;\n }\n }\n}\n\n// Allow for dropdowns to go bottom up (aka, dropup-menu)\n// Just add .dropup after the standard .dropdown class and you're set.\n.dropup {\n .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(up);\n }\n}\n\n.dropright {\n .dropdown-menu {\n top: 0;\n right: auto;\n left: 100%;\n margin-top: 0;\n margin-left: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(right);\n &::after {\n vertical-align: 0;\n }\n }\n}\n\n.dropleft {\n .dropdown-menu {\n top: 0;\n right: 100%;\n left: auto;\n margin-top: 0;\n margin-right: $dropdown-spacer;\n }\n\n .dropdown-toggle {\n @include caret(left);\n &::before {\n vertical-align: 0;\n }\n }\n}\n\n// When enabled Popper.js, reset basic dropdown position\n// stylelint-disable-next-line no-duplicate-selectors\n.dropdown-menu {\n &[x-placement^=\"top\"],\n &[x-placement^=\"right\"],\n &[x-placement^=\"bottom\"],\n &[x-placement^=\"left\"] {\n right: auto;\n bottom: auto;\n }\n}\n\n// Dividers (basically an `<hr>`) within the dropdown\n.dropdown-divider {\n @include nav-divider($dropdown-divider-bg, $dropdown-divider-margin-y, true);\n}\n\n// Links, buttons, and more within the dropdown menu\n//\n// `<button>`-specific styles are denoted with `// For <button>s`\n.dropdown-item {\n display: block;\n width: 100%; // For `<button>`s\n padding: $dropdown-item-padding-y $dropdown-item-padding-x;\n clear: both;\n font-weight: $font-weight-normal;\n color: $dropdown-link-color;\n text-align: inherit; // For `<button>`s\n text-decoration: if($link-decoration == none, null, none);\n white-space: nowrap; // prevent links from randomly breaking onto new lines\n background-color: transparent; // For `<button>`s\n border: 0; // For `<button>`s\n\n // Prevent dropdown overflow if there's no padding\n // See https://github.com/twbs/bootstrap/pull/27703\n @if $dropdown-padding-y == 0 {\n &:first-child {\n @include border-top-radius($dropdown-inner-border-radius);\n }\n\n &:last-child {\n @include border-bottom-radius($dropdown-inner-border-radius);\n }\n }\n\n @include hover-focus() {\n color: $dropdown-link-hover-color;\n text-decoration: none;\n @include gradient-bg($dropdown-link-hover-bg);\n }\n\n &.active,\n &:active {\n color: $dropdown-link-active-color;\n text-decoration: none;\n @include gradient-bg($dropdown-link-active-bg);\n }\n\n &.disabled,\n &:disabled {\n color: $dropdown-link-disabled-color;\n pointer-events: none;\n background-color: transparent;\n // Remove CSS gradients if they're enabled\n @if $enable-gradients {\n background-image: none;\n }\n }\n}\n\n.dropdown-menu.show {\n display: block;\n}\n\n// Dropdown section headers\n.dropdown-header {\n display: block;\n padding: $dropdown-header-padding;\n margin-bottom: 0; // for use with heading elements\n @include font-size($font-size-sm);\n color: $dropdown-header-color;\n white-space: nowrap; // as with > li > a\n}\n\n// Dropdown text\n.dropdown-item-text {\n display: block;\n padding: $dropdown-item-padding-y $dropdown-item-padding-x;\n color: $dropdown-link-color;\n}\n","@mixin caret-down() {\n border-top: $caret-width solid;\n border-right: $caret-width solid transparent;\n border-bottom: 0;\n border-left: $caret-width solid transparent;\n}\n\n@mixin caret-up() {\n border-top: 0;\n border-right: $caret-width solid transparent;\n border-bottom: $caret-width solid;\n border-left: $caret-width solid transparent;\n}\n\n@mixin caret-right() {\n border-top: $caret-width solid transparent;\n border-right: 0;\n border-bottom: $caret-width solid transparent;\n border-left: $caret-width solid;\n}\n\n@mixin caret-left() {\n border-top: $caret-width solid transparent;\n border-right: $caret-width solid;\n border-bottom: $caret-width solid transparent;\n}\n\n@mixin caret($direction: down) {\n @if $enable-caret {\n &::after {\n display: inline-block;\n margin-left: $caret-spacing;\n vertical-align: $caret-vertical-align;\n content: \"\";\n @if $direction == down {\n @include caret-down();\n } @else if $direction == up {\n @include caret-up();\n } @else if $direction == right {\n @include caret-right();\n }\n }\n\n @if $direction == left {\n &::after {\n display: none;\n }\n\n &::before {\n display: inline-block;\n margin-right: $caret-spacing;\n vertical-align: $caret-vertical-align;\n content: \"\";\n @include caret-left();\n }\n }\n\n &:empty::after {\n margin-left: 0;\n }\n }\n}\n","// Horizontal dividers\n//\n// Dividers (basically an hr) within dropdowns and nav lists\n\n@mixin nav-divider($color: $nav-divider-color, $margin-y: $nav-divider-margin-y, $ignore-warning: false) {\n height: 0;\n margin: $margin-y 0;\n overflow: hidden;\n border-top: 1px solid $color;\n @include deprecate(\"The `nav-divider()` mixin\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n","// stylelint-disable selector-no-qualifying-type\n\n// Make the div behave like a button\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-flex;\n vertical-align: middle; // match .btn alignment given font-size hack above\n\n > .btn {\n position: relative;\n flex: 1 1 auto;\n\n // Bring the hover, focused, and \"active\" buttons to the front to overlay\n // the borders properly\n @include hover() {\n z-index: 1;\n }\n &:focus,\n &:active,\n &.active {\n z-index: 1;\n }\n }\n}\n\n// Optional: Group multiple button groups together for a toolbar\n.btn-toolbar {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-start;\n\n .input-group {\n width: auto;\n }\n}\n\n.btn-group {\n // Prevent double borders when buttons are next to each other\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) {\n margin-left: -$btn-border-width;\n }\n\n // Reset rounded corners\n > .btn:not(:last-child):not(.dropdown-toggle),\n > .btn-group:not(:last-child) > .btn {\n @include border-right-radius(0);\n }\n\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) > .btn {\n @include border-left-radius(0);\n }\n}\n\n// Sizing\n//\n// Remix the default button sizing classes into new ones for easier manipulation.\n\n.btn-group-sm > .btn { @extend .btn-sm; }\n.btn-group-lg > .btn { @extend .btn-lg; }\n\n\n//\n// Split button dropdowns\n//\n\n.dropdown-toggle-split {\n padding-right: $btn-padding-x * .75;\n padding-left: $btn-padding-x * .75;\n\n &::after,\n .dropup &::after,\n .dropright &::after {\n margin-left: 0;\n }\n\n .dropleft &::before {\n margin-right: 0;\n }\n}\n\n.btn-sm + .dropdown-toggle-split {\n padding-right: $btn-padding-x-sm * .75;\n padding-left: $btn-padding-x-sm * .75;\n}\n\n.btn-lg + .dropdown-toggle-split {\n padding-right: $btn-padding-x-lg * .75;\n padding-left: $btn-padding-x-lg * .75;\n}\n\n\n// The clickable button for toggling the menu\n// Set the same inset shadow as the :active state\n.btn-group.show .dropdown-toggle {\n @include box-shadow($btn-active-box-shadow);\n\n // Show no shadow for `.btn-link` since it has no other button styles.\n &.btn-link {\n @include box-shadow(none);\n }\n}\n\n\n//\n// Vertical button groups\n//\n\n.btn-group-vertical {\n flex-direction: column;\n align-items: flex-start;\n justify-content: center;\n\n > .btn,\n > .btn-group {\n width: 100%;\n }\n\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) {\n margin-top: -$btn-border-width;\n }\n\n // Reset rounded corners\n > .btn:not(:last-child):not(.dropdown-toggle),\n > .btn-group:not(:last-child) > .btn {\n @include border-bottom-radius(0);\n }\n\n > .btn:not(:first-child),\n > .btn-group:not(:first-child) > .btn {\n @include border-top-radius(0);\n }\n}\n\n\n// Checkbox and radio options\n//\n// In order to support the browser's form validation feedback, powered by the\n// `required` attribute, we have to \"hide\" the inputs via `clip`. We cannot use\n// `display: none;` or `visibility: hidden;` as that also hides the popover.\n// Simply visually hiding the inputs via `opacity` would leave them clickable in\n// certain cases which is prevented by using `clip` and `pointer-events`.\n// This way, we ensure a DOM element is visible to position the popover from.\n//\n// See https://github.com/twbs/bootstrap/pull/12794 and\n// https://github.com/twbs/bootstrap/pull/14559 for more information.\n\n.btn-group-toggle {\n > .btn,\n > .btn-group > .btn {\n margin-bottom: 0; // Override default `<label>` value\n\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n }\n }\n}\n","// stylelint-disable selector-no-qualifying-type\n\n//\n// Base styles\n//\n\n.input-group {\n position: relative;\n display: flex;\n flex-wrap: wrap; // For form validation feedback\n align-items: stretch;\n width: 100%;\n\n > .form-control,\n > .form-control-plaintext,\n > .custom-select,\n > .custom-file {\n position: relative; // For focus state's z-index\n flex: 1 1 auto;\n width: 1%;\n min-width: 0; // https://stackoverflow.com/questions/36247140/why-dont-flex-items-shrink-past-content-size\n margin-bottom: 0;\n\n + .form-control,\n + .custom-select,\n + .custom-file {\n margin-left: -$input-border-width;\n }\n }\n\n // Bring the \"active\" form control to the top of surrounding elements\n > .form-control:focus,\n > .custom-select:focus,\n > .custom-file .custom-file-input:focus ~ .custom-file-label {\n z-index: 3;\n }\n\n // Bring the custom file input above the label\n > .custom-file .custom-file-input:focus {\n z-index: 4;\n }\n\n > .form-control,\n > .custom-select {\n &:not(:last-child) { @include border-right-radius(0); }\n &:not(:first-child) { @include border-left-radius(0); }\n }\n\n // Custom file inputs have more complex markup, thus requiring different\n // border-radius overrides.\n > .custom-file {\n display: flex;\n align-items: center;\n\n &:not(:last-child) .custom-file-label,\n &:not(:last-child) .custom-file-label::after { @include border-right-radius(0); }\n &:not(:first-child) .custom-file-label { @include border-left-radius(0); }\n }\n}\n\n\n// Prepend and append\n//\n// While it requires one extra layer of HTML for each, dedicated prepend and\n// append elements allow us to 1) be less clever, 2) simplify our selectors, and\n// 3) support HTML5 form validation.\n\n.input-group-prepend,\n.input-group-append {\n display: flex;\n\n // Ensure buttons are always above inputs for more visually pleasing borders.\n // This isn't needed for `.input-group-text` since it shares the same border-color\n // as our inputs.\n .btn {\n position: relative;\n z-index: 2;\n\n &:focus {\n z-index: 3;\n }\n }\n\n .btn + .btn,\n .btn + .input-group-text,\n .input-group-text + .input-group-text,\n .input-group-text + .btn {\n margin-left: -$input-border-width;\n }\n}\n\n.input-group-prepend { margin-right: -$input-border-width; }\n.input-group-append { margin-left: -$input-border-width; }\n\n\n// Textual addons\n//\n// Serves as a catch-all element for any text or radio/checkbox input you wish\n// to prepend or append to an input.\n\n.input-group-text {\n display: flex;\n align-items: center;\n padding: $input-padding-y $input-padding-x;\n margin-bottom: 0; // Allow use of <label> elements by overriding our default margin-bottom\n @include font-size($input-font-size); // Match inputs\n font-weight: $font-weight-normal;\n line-height: $input-line-height;\n color: $input-group-addon-color;\n text-align: center;\n white-space: nowrap;\n background-color: $input-group-addon-bg;\n border: $input-border-width solid $input-group-addon-border-color;\n @include border-radius($input-border-radius);\n\n // Nuke default margins from checkboxes and radios to vertically center within.\n input[type=\"radio\"],\n input[type=\"checkbox\"] {\n margin-top: 0;\n }\n}\n\n\n// Sizing\n//\n// Remix the default form control sizing classes into new ones for easier\n// manipulation.\n\n.input-group-lg > .form-control:not(textarea),\n.input-group-lg > .custom-select {\n height: $input-height-lg;\n}\n\n.input-group-lg > .form-control,\n.input-group-lg > .custom-select,\n.input-group-lg > .input-group-prepend > .input-group-text,\n.input-group-lg > .input-group-append > .input-group-text,\n.input-group-lg > .input-group-prepend > .btn,\n.input-group-lg > .input-group-append > .btn {\n padding: $input-padding-y-lg $input-padding-x-lg;\n @include font-size($input-font-size-lg);\n line-height: $input-line-height-lg;\n @include border-radius($input-border-radius-lg);\n}\n\n.input-group-sm > .form-control:not(textarea),\n.input-group-sm > .custom-select {\n height: $input-height-sm;\n}\n\n.input-group-sm > .form-control,\n.input-group-sm > .custom-select,\n.input-group-sm > .input-group-prepend > .input-group-text,\n.input-group-sm > .input-group-append > .input-group-text,\n.input-group-sm > .input-group-prepend > .btn,\n.input-group-sm > .input-group-append > .btn {\n padding: $input-padding-y-sm $input-padding-x-sm;\n @include font-size($input-font-size-sm);\n line-height: $input-line-height-sm;\n @include border-radius($input-border-radius-sm);\n}\n\n.input-group-lg > .custom-select,\n.input-group-sm > .custom-select {\n padding-right: $custom-select-padding-x + $custom-select-indicator-padding;\n}\n\n\n// Prepend and append rounded corners\n//\n// These rulesets must come after the sizing ones to properly override sm and lg\n// border-radius values when extending. They're more specific than we'd like\n// with the `.input-group >` part, but without it, we cannot override the sizing.\n\n\n.input-group > .input-group-prepend > .btn,\n.input-group > .input-group-prepend > .input-group-text,\n.input-group > .input-group-append:not(:last-child) > .btn,\n.input-group > .input-group-append:not(:last-child) > .input-group-text,\n.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {\n @include border-right-radius(0);\n}\n\n.input-group > .input-group-append > .btn,\n.input-group > .input-group-append > .input-group-text,\n.input-group > .input-group-prepend:not(:first-child) > .btn,\n.input-group > .input-group-prepend:not(:first-child) > .input-group-text,\n.input-group > .input-group-prepend:first-child > .btn:not(:first-child),\n.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {\n @include border-left-radius(0);\n}\n","// Embedded icons from Open Iconic.\n// Released under MIT and copyright 2014 Waybury.\n// https://useiconic.com/open\n\n\n// Checkboxes and radios\n//\n// Base class takes care of all the key behavioral aspects.\n\n.custom-control {\n position: relative;\n display: block;\n min-height: $font-size-base * $line-height-base;\n padding-left: $custom-control-gutter + $custom-control-indicator-size;\n}\n\n.custom-control-inline {\n display: inline-flex;\n margin-right: $custom-control-spacer-x;\n}\n\n.custom-control-input {\n position: absolute;\n left: 0;\n z-index: -1; // Put the input behind the label so it doesn't overlay text\n width: $custom-control-indicator-size;\n height: ($font-size-base * $line-height-base + $custom-control-indicator-size) / 2;\n opacity: 0;\n\n &:checked ~ .custom-control-label::before {\n color: $custom-control-indicator-checked-color;\n border-color: $custom-control-indicator-checked-border-color;\n @include gradient-bg($custom-control-indicator-checked-bg);\n @include box-shadow($custom-control-indicator-checked-box-shadow);\n }\n\n &:focus ~ .custom-control-label::before {\n // the mixin is not used here to make sure there is feedback\n @if $enable-shadows {\n box-shadow: $input-box-shadow, $input-focus-box-shadow;\n } @else {\n box-shadow: $custom-control-indicator-focus-box-shadow;\n }\n }\n\n &:focus:not(:checked) ~ .custom-control-label::before {\n border-color: $custom-control-indicator-focus-border-color;\n }\n\n &:not(:disabled):active ~ .custom-control-label::before {\n color: $custom-control-indicator-active-color;\n background-color: $custom-control-indicator-active-bg;\n border-color: $custom-control-indicator-active-border-color;\n @include box-shadow($custom-control-indicator-active-box-shadow);\n }\n\n // Use [disabled] and :disabled to work around https://github.com/twbs/bootstrap/issues/28247\n &[disabled],\n &:disabled {\n ~ .custom-control-label {\n color: $custom-control-label-disabled-color;\n\n &::before {\n background-color: $custom-control-indicator-disabled-bg;\n }\n }\n }\n}\n\n// Custom control indicators\n//\n// Build the custom controls out of pseudo-elements.\n\n.custom-control-label {\n position: relative;\n margin-bottom: 0;\n color: $custom-control-label-color;\n vertical-align: top;\n cursor: $custom-control-cursor;\n\n // Background-color and (when enabled) gradient\n &::before {\n position: absolute;\n top: ($font-size-base * $line-height-base - $custom-control-indicator-size) / 2;\n left: -($custom-control-gutter + $custom-control-indicator-size);\n display: block;\n width: $custom-control-indicator-size;\n height: $custom-control-indicator-size;\n pointer-events: none;\n content: \"\";\n background-color: $custom-control-indicator-bg;\n border: $custom-control-indicator-border-color solid $custom-control-indicator-border-width;\n @include box-shadow($custom-control-indicator-box-shadow);\n }\n\n // Foreground (icon)\n &::after {\n position: absolute;\n top: ($font-size-base * $line-height-base - $custom-control-indicator-size) / 2;\n left: -($custom-control-gutter + $custom-control-indicator-size);\n display: block;\n width: $custom-control-indicator-size;\n height: $custom-control-indicator-size;\n content: \"\";\n background: no-repeat 50% / #{$custom-control-indicator-bg-size};\n }\n}\n\n\n// Checkboxes\n//\n// Tweak just a few things for checkboxes.\n\n.custom-checkbox {\n .custom-control-label::before {\n @include border-radius($custom-checkbox-indicator-border-radius);\n }\n\n .custom-control-input:checked ~ .custom-control-label {\n &::after {\n background-image: escape-svg($custom-checkbox-indicator-icon-checked);\n }\n }\n\n .custom-control-input:indeterminate ~ .custom-control-label {\n &::before {\n border-color: $custom-checkbox-indicator-indeterminate-border-color;\n @include gradient-bg($custom-checkbox-indicator-indeterminate-bg);\n @include box-shadow($custom-checkbox-indicator-indeterminate-box-shadow);\n }\n &::after {\n background-image: escape-svg($custom-checkbox-indicator-icon-indeterminate);\n }\n }\n\n .custom-control-input:disabled {\n &:checked ~ .custom-control-label::before {\n background-color: $custom-control-indicator-checked-disabled-bg;\n }\n &:indeterminate ~ .custom-control-label::before {\n background-color: $custom-control-indicator-checked-disabled-bg;\n }\n }\n}\n\n// Radios\n//\n// Tweak just a few things for radios.\n\n.custom-radio {\n .custom-control-label::before {\n // stylelint-disable-next-line property-blacklist\n border-radius: $custom-radio-indicator-border-radius;\n }\n\n .custom-control-input:checked ~ .custom-control-label {\n &::after {\n background-image: escape-svg($custom-radio-indicator-icon-checked);\n }\n }\n\n .custom-control-input:disabled {\n &:checked ~ .custom-control-label::before {\n background-color: $custom-control-indicator-checked-disabled-bg;\n }\n }\n}\n\n\n// switches\n//\n// Tweak a few things for switches\n\n.custom-switch {\n padding-left: $custom-switch-width + $custom-control-gutter;\n\n .custom-control-label {\n &::before {\n left: -($custom-switch-width + $custom-control-gutter);\n width: $custom-switch-width;\n pointer-events: all;\n // stylelint-disable-next-line property-blacklist\n border-radius: $custom-switch-indicator-border-radius;\n }\n\n &::after {\n top: add(($font-size-base * $line-height-base - $custom-control-indicator-size) / 2, $custom-control-indicator-border-width * 2);\n left: add(-($custom-switch-width + $custom-control-gutter), $custom-control-indicator-border-width * 2);\n width: $custom-switch-indicator-size;\n height: $custom-switch-indicator-size;\n background-color: $custom-control-indicator-border-color;\n // stylelint-disable-next-line property-blacklist\n border-radius: $custom-switch-indicator-border-radius;\n @include transition(transform .15s ease-in-out, $custom-forms-transition);\n }\n }\n\n .custom-control-input:checked ~ .custom-control-label {\n &::after {\n background-color: $custom-control-indicator-bg;\n transform: translateX($custom-switch-width - $custom-control-indicator-size);\n }\n }\n\n .custom-control-input:disabled {\n &:checked ~ .custom-control-label::before {\n background-color: $custom-control-indicator-checked-disabled-bg;\n }\n }\n}\n\n\n// Select\n//\n// Replaces the browser default select with a custom one, mostly pulled from\n// https://primer.github.io/.\n//\n\n.custom-select {\n display: inline-block;\n width: 100%;\n height: $custom-select-height;\n padding: $custom-select-padding-y ($custom-select-padding-x + $custom-select-indicator-padding) $custom-select-padding-y $custom-select-padding-x;\n font-family: $custom-select-font-family;\n @include font-size($custom-select-font-size);\n font-weight: $custom-select-font-weight;\n line-height: $custom-select-line-height;\n color: $custom-select-color;\n vertical-align: middle;\n background: $custom-select-bg $custom-select-background;\n border: $custom-select-border-width solid $custom-select-border-color;\n @include border-radius($custom-select-border-radius, 0);\n @include box-shadow($custom-select-box-shadow);\n appearance: none;\n\n &:focus {\n border-color: $custom-select-focus-border-color;\n outline: 0;\n @if $enable-shadows {\n @include box-shadow($custom-select-box-shadow, $custom-select-focus-box-shadow);\n } @else {\n // Avoid using mixin so we can pass custom focus shadow properly\n box-shadow: $custom-select-focus-box-shadow;\n }\n\n &::-ms-value {\n // For visual consistency with other platforms/browsers,\n // suppress the default white text on blue background highlight given to\n // the selected option text when the (still closed) <select> receives focus\n // in IE and (under certain conditions) Edge.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n }\n\n &[multiple],\n &[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: $custom-select-padding-x;\n background-image: none;\n }\n\n &:disabled {\n color: $custom-select-disabled-color;\n background-color: $custom-select-disabled-bg;\n }\n\n // Hides the default caret in IE11\n &::-ms-expand {\n display: none;\n }\n\n // Remove outline from select box in FF\n &:-moz-focusring {\n color: transparent;\n text-shadow: 0 0 0 $custom-select-color;\n }\n}\n\n.custom-select-sm {\n height: $custom-select-height-sm;\n padding-top: $custom-select-padding-y-sm;\n padding-bottom: $custom-select-padding-y-sm;\n padding-left: $custom-select-padding-x-sm;\n @include font-size($custom-select-font-size-sm);\n}\n\n.custom-select-lg {\n height: $custom-select-height-lg;\n padding-top: $custom-select-padding-y-lg;\n padding-bottom: $custom-select-padding-y-lg;\n padding-left: $custom-select-padding-x-lg;\n @include font-size($custom-select-font-size-lg);\n}\n\n\n// File\n//\n// Custom file input.\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: $custom-file-height;\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: $custom-file-height;\n margin: 0;\n opacity: 0;\n\n &:focus ~ .custom-file-label {\n border-color: $custom-file-focus-border-color;\n box-shadow: $custom-file-focus-box-shadow;\n }\n\n // Use [disabled] and :disabled to work around https://github.com/twbs/bootstrap/issues/28247\n &[disabled] ~ .custom-file-label,\n &:disabled ~ .custom-file-label {\n background-color: $custom-file-disabled-bg;\n }\n\n @each $lang, $value in $custom-file-text {\n &:lang(#{$lang}) ~ .custom-file-label::after {\n content: $value;\n }\n }\n\n ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n }\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: $custom-file-height;\n padding: $custom-file-padding-y $custom-file-padding-x;\n font-family: $custom-file-font-family;\n font-weight: $custom-file-font-weight;\n line-height: $custom-file-line-height;\n color: $custom-file-color;\n background-color: $custom-file-bg;\n border: $custom-file-border-width solid $custom-file-border-color;\n @include border-radius($custom-file-border-radius);\n @include box-shadow($custom-file-box-shadow);\n\n &::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: $custom-file-height-inner;\n padding: $custom-file-padding-y $custom-file-padding-x;\n line-height: $custom-file-line-height;\n color: $custom-file-button-color;\n content: \"Browse\";\n @include gradient-bg($custom-file-button-bg);\n border-left: inherit;\n @include border-radius(0 $custom-file-border-radius $custom-file-border-radius 0);\n }\n}\n\n// Range\n//\n// Style range inputs the same across browsers. Vendor-specific rules for pseudo\n// elements cannot be mixed. As such, there are no shared styles for focus or\n// active states on prefixed selectors.\n\n.custom-range {\n width: 100%;\n height: add($custom-range-thumb-height, $custom-range-thumb-focus-box-shadow-width * 2);\n padding: 0; // Need to reset padding\n background-color: transparent;\n appearance: none;\n\n &:focus {\n outline: none;\n\n // Pseudo-elements must be split across multiple rulesets to have an effect.\n // No box-shadow() mixin for focus accessibility.\n &::-webkit-slider-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n &::-moz-range-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n &::-ms-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n }\n\n &::-moz-focus-outer {\n border: 0;\n }\n\n &::-webkit-slider-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n margin-top: ($custom-range-track-height - $custom-range-thumb-height) / 2; // Webkit specific\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-webkit-slider-runnable-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent; // Why?\n cursor: $custom-range-track-cursor;\n background-color: $custom-range-track-bg;\n border-color: transparent;\n @include border-radius($custom-range-track-border-radius);\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-moz-range-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-moz-range-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent;\n cursor: $custom-range-track-cursor;\n background-color: $custom-range-track-bg;\n border-color: transparent; // Firefox specific?\n @include border-radius($custom-range-track-border-radius);\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-ms-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n margin-top: 0; // Edge specific\n margin-right: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.\n margin-left: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-ms-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent;\n cursor: $custom-range-track-cursor;\n background-color: transparent;\n border-color: transparent;\n border-width: $custom-range-thumb-height / 2;\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-ms-fill-lower {\n background-color: $custom-range-track-bg;\n @include border-radius($custom-range-track-border-radius);\n }\n\n &::-ms-fill-upper {\n margin-right: 15px; // arbitrary?\n background-color: $custom-range-track-bg;\n @include border-radius($custom-range-track-border-radius);\n }\n\n &:disabled {\n &::-webkit-slider-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n\n &::-webkit-slider-runnable-track {\n cursor: default;\n }\n\n &::-moz-range-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n\n &::-moz-range-track {\n cursor: default;\n }\n\n &::-ms-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n }\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n @include transition($custom-forms-transition);\n}\n","// Base class\n//\n// Kickstart any navigation component with a set of style resets. Works with\n// `<nav>`s, `<ul>`s or `<ol>`s.\n\n.nav {\n display: flex;\n flex-wrap: wrap;\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n\n.nav-link {\n display: block;\n padding: $nav-link-padding-y $nav-link-padding-x;\n text-decoration: if($link-decoration == none, null, none);\n\n @include hover-focus() {\n text-decoration: none;\n }\n\n // Disabled state lightens text\n &.disabled {\n color: $nav-link-disabled-color;\n pointer-events: none;\n cursor: default;\n }\n}\n\n//\n// Tabs\n//\n\n.nav-tabs {\n border-bottom: $nav-tabs-border-width solid $nav-tabs-border-color;\n\n .nav-item {\n margin-bottom: -$nav-tabs-border-width;\n }\n\n .nav-link {\n border: $nav-tabs-border-width solid transparent;\n @include border-top-radius($nav-tabs-border-radius);\n\n @include hover-focus() {\n border-color: $nav-tabs-link-hover-border-color;\n }\n\n &.disabled {\n color: $nav-link-disabled-color;\n background-color: transparent;\n border-color: transparent;\n }\n }\n\n .nav-link.active,\n .nav-item.show .nav-link {\n color: $nav-tabs-link-active-color;\n background-color: $nav-tabs-link-active-bg;\n border-color: $nav-tabs-link-active-border-color;\n }\n\n .dropdown-menu {\n // Make dropdown border overlap tab border\n margin-top: -$nav-tabs-border-width;\n // Remove the top rounded corners here since there is a hard edge above the menu\n @include border-top-radius(0);\n }\n}\n\n\n//\n// Pills\n//\n\n.nav-pills {\n .nav-link {\n @include border-radius($nav-pills-border-radius);\n }\n\n .nav-link.active,\n .show > .nav-link {\n color: $nav-pills-link-active-color;\n background-color: $nav-pills-link-active-bg;\n }\n}\n\n\n//\n// Justified variants\n//\n\n.nav-fill {\n .nav-item {\n flex: 1 1 auto;\n text-align: center;\n }\n}\n\n.nav-justified {\n .nav-item {\n flex-basis: 0;\n flex-grow: 1;\n text-align: center;\n }\n}\n\n\n// Tabbable tabs\n//\n// Hide tabbable panes to start, show them when `.active`\n\n.tab-content {\n > .tab-pane {\n display: none;\n }\n > .active {\n display: block;\n }\n}\n","// Contents\n//\n// Navbar\n// Navbar brand\n// Navbar nav\n// Navbar text\n// Navbar divider\n// Responsive navbar\n// Navbar position\n// Navbar themes\n\n\n// Navbar\n//\n// Provide a static navbar from which we expand to create full-width, fixed, and\n// other navbar variations.\n\n.navbar {\n position: relative;\n display: flex;\n flex-wrap: wrap; // allow us to do the line break for collapsing content\n align-items: center;\n justify-content: space-between; // space out brand from logo\n padding: $navbar-padding-y $navbar-padding-x;\n\n // Because flex properties aren't inherited, we need to redeclare these first\n // few properties so that content nested within behave properly.\n %container-flex-properties {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n }\n\n .container,\n .container-fluid {\n @extend %container-flex-properties;\n }\n\n @each $breakpoint, $container-max-width in $container-max-widths {\n > .container#{breakpoint-infix($breakpoint, $container-max-widths)} {\n @extend %container-flex-properties;\n }\n }\n}\n\n\n// Navbar brand\n//\n// Used for brand, project, or site names.\n\n.navbar-brand {\n display: inline-block;\n padding-top: $navbar-brand-padding-y;\n padding-bottom: $navbar-brand-padding-y;\n margin-right: $navbar-padding-x;\n @include font-size($navbar-brand-font-size);\n line-height: inherit;\n white-space: nowrap;\n\n @include hover-focus() {\n text-decoration: none;\n }\n}\n\n\n// Navbar nav\n//\n// Custom navbar navigation (doesn't require `.nav`, but does make use of `.nav-link`).\n\n.navbar-nav {\n display: flex;\n flex-direction: column; // cannot use `inherit` to get the `.navbar`s value\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n\n .nav-link {\n padding-right: 0;\n padding-left: 0;\n }\n\n .dropdown-menu {\n position: static;\n float: none;\n }\n}\n\n\n// Navbar text\n//\n//\n\n.navbar-text {\n display: inline-block;\n padding-top: $nav-link-padding-y;\n padding-bottom: $nav-link-padding-y;\n}\n\n\n// Responsive navbar\n//\n// Custom styles for responsive collapsing and toggling of navbar contents.\n// Powered by the collapse Bootstrap JavaScript plugin.\n\n// When collapsed, prevent the toggleable navbar contents from appearing in\n// the default flexbox row orientation. Requires the use of `flex-wrap: wrap`\n// on the `.navbar` parent.\n.navbar-collapse {\n flex-basis: 100%;\n flex-grow: 1;\n // For always expanded or extra full navbars, ensure content aligns itself\n // properly vertically. Can be easily overridden with flex utilities.\n align-items: center;\n}\n\n// Button for toggling the navbar when in its collapsed state\n.navbar-toggler {\n padding: $navbar-toggler-padding-y $navbar-toggler-padding-x;\n @include font-size($navbar-toggler-font-size);\n line-height: 1;\n background-color: transparent; // remove default button style\n border: $border-width solid transparent; // remove default button style\n @include border-radius($navbar-toggler-border-radius);\n\n @include hover-focus() {\n text-decoration: none;\n }\n}\n\n// Keep as a separate element so folks can easily override it with another icon\n// or image file as needed.\n.navbar-toggler-icon {\n display: inline-block;\n width: 1.5em;\n height: 1.5em;\n vertical-align: middle;\n content: \"\";\n background: no-repeat center center;\n background-size: 100% 100%;\n}\n\n// Generate series of `.navbar-expand-*` responsive classes for configuring\n// where your navbar collapses.\n.navbar-expand {\n @each $breakpoint in map-keys($grid-breakpoints) {\n $next: breakpoint-next($breakpoint, $grid-breakpoints);\n $infix: breakpoint-infix($next, $grid-breakpoints);\n\n &#{$infix} {\n @include media-breakpoint-down($breakpoint) {\n %container-navbar-expand-#{$breakpoint} {\n padding-right: 0;\n padding-left: 0;\n }\n\n > .container,\n > .container-fluid {\n @extend %container-navbar-expand-#{$breakpoint};\n }\n\n @each $size, $container-max-width in $container-max-widths {\n > .container#{breakpoint-infix($size, $container-max-widths)} {\n @extend %container-navbar-expand-#{$breakpoint};\n }\n }\n }\n\n @include media-breakpoint-up($next) {\n flex-flow: row nowrap;\n justify-content: flex-start;\n\n .navbar-nav {\n flex-direction: row;\n\n .dropdown-menu {\n position: absolute;\n }\n\n .nav-link {\n padding-right: $navbar-nav-link-padding-x;\n padding-left: $navbar-nav-link-padding-x;\n }\n }\n\n // For nesting containers, have to redeclare for alignment purposes\n %container-nesting-#{$breakpoint} {\n flex-wrap: nowrap;\n }\n\n > .container,\n > .container-fluid {\n @extend %container-nesting-#{$breakpoint};\n }\n\n @each $size, $container-max-width in $container-max-widths {\n > .container#{breakpoint-infix($size, $container-max-widths)} {\n @extend %container-nesting-#{$breakpoint};\n }\n }\n\n .navbar-collapse {\n display: flex !important; // stylelint-disable-line declaration-no-important\n\n // Changes flex-bases to auto because of an IE10 bug\n flex-basis: auto;\n }\n\n .navbar-toggler {\n display: none;\n }\n }\n }\n }\n}\n\n\n// Navbar themes\n//\n// Styles for switching between navbars with light or dark background.\n\n// Dark links against a light background\n.navbar-light {\n .navbar-brand {\n color: $navbar-light-brand-color;\n\n @include hover-focus() {\n color: $navbar-light-brand-hover-color;\n }\n }\n\n .navbar-nav {\n .nav-link {\n color: $navbar-light-color;\n\n @include hover-focus() {\n color: $navbar-light-hover-color;\n }\n\n &.disabled {\n color: $navbar-light-disabled-color;\n }\n }\n\n .show > .nav-link,\n .active > .nav-link,\n .nav-link.show,\n .nav-link.active {\n color: $navbar-light-active-color;\n }\n }\n\n .navbar-toggler {\n color: $navbar-light-color;\n border-color: $navbar-light-toggler-border-color;\n }\n\n .navbar-toggler-icon {\n background-image: escape-svg($navbar-light-toggler-icon-bg);\n }\n\n .navbar-text {\n color: $navbar-light-color;\n a {\n color: $navbar-light-active-color;\n\n @include hover-focus() {\n color: $navbar-light-active-color;\n }\n }\n }\n}\n\n// White links against a dark background\n.navbar-dark {\n .navbar-brand {\n color: $navbar-dark-brand-color;\n\n @include hover-focus() {\n color: $navbar-dark-brand-hover-color;\n }\n }\n\n .navbar-nav {\n .nav-link {\n color: $navbar-dark-color;\n\n @include hover-focus() {\n color: $navbar-dark-hover-color;\n }\n\n &.disabled {\n color: $navbar-dark-disabled-color;\n }\n }\n\n .show > .nav-link,\n .active > .nav-link,\n .nav-link.show,\n .nav-link.active {\n color: $navbar-dark-active-color;\n }\n }\n\n .navbar-toggler {\n color: $navbar-dark-color;\n border-color: $navbar-dark-toggler-border-color;\n }\n\n .navbar-toggler-icon {\n background-image: escape-svg($navbar-dark-toggler-icon-bg);\n }\n\n .navbar-text {\n color: $navbar-dark-color;\n a {\n color: $navbar-dark-active-color;\n\n @include hover-focus() {\n color: $navbar-dark-active-color;\n }\n }\n }\n}\n","//\n// Base styles\n//\n\n.card {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106\n height: $card-height;\n word-wrap: break-word;\n background-color: $card-bg;\n background-clip: border-box;\n border: $card-border-width solid $card-border-color;\n @include border-radius($card-border-radius);\n\n > hr {\n margin-right: 0;\n margin-left: 0;\n }\n\n > .list-group {\n border-top: inherit;\n border-bottom: inherit;\n\n &:first-child {\n border-top-width: 0;\n @include border-top-radius($card-inner-border-radius);\n }\n\n &:last-child {\n border-bottom-width: 0;\n @include border-bottom-radius($card-inner-border-radius);\n }\n }\n}\n\n.card-body {\n // Enable `flex-grow: 1` for decks and groups so that card blocks take up\n // as much space as possible, ensuring footers are aligned to the bottom.\n flex: 1 1 auto;\n // Workaround for the image size bug in IE\n // See: https://github.com/twbs/bootstrap/pull/28855\n min-height: 1px;\n padding: $card-spacer-x;\n color: $card-color;\n}\n\n.card-title {\n margin-bottom: $card-spacer-y;\n}\n\n.card-subtitle {\n margin-top: -$card-spacer-y / 2;\n margin-bottom: 0;\n}\n\n.card-text:last-child {\n margin-bottom: 0;\n}\n\n.card-link {\n @include hover() {\n text-decoration: none;\n }\n\n + .card-link {\n margin-left: $card-spacer-x;\n }\n}\n\n//\n// Optional textual caps\n//\n\n.card-header {\n padding: $card-spacer-y $card-spacer-x;\n margin-bottom: 0; // Removes the default margin-bottom of <hN>\n color: $card-cap-color;\n background-color: $card-cap-bg;\n border-bottom: $card-border-width solid $card-border-color;\n\n &:first-child {\n @include border-radius($card-inner-border-radius $card-inner-border-radius 0 0);\n }\n\n + .list-group {\n .list-group-item:first-child {\n border-top: 0;\n }\n }\n}\n\n.card-footer {\n padding: $card-spacer-y $card-spacer-x;\n color: $card-cap-color;\n background-color: $card-cap-bg;\n border-top: $card-border-width solid $card-border-color;\n\n &:last-child {\n @include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius);\n }\n}\n\n\n//\n// Header navs\n//\n\n.card-header-tabs {\n margin-right: -$card-spacer-x / 2;\n margin-bottom: -$card-spacer-y;\n margin-left: -$card-spacer-x / 2;\n border-bottom: 0;\n}\n\n.card-header-pills {\n margin-right: -$card-spacer-x / 2;\n margin-left: -$card-spacer-x / 2;\n}\n\n// Card image\n.card-img-overlay {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n padding: $card-img-overlay-padding;\n}\n\n.card-img,\n.card-img-top,\n.card-img-bottom {\n flex-shrink: 0; // For IE: https://github.com/twbs/bootstrap/issues/29396\n width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch\n}\n\n.card-img,\n.card-img-top {\n @include border-top-radius($card-inner-border-radius);\n}\n\n.card-img,\n.card-img-bottom {\n @include border-bottom-radius($card-inner-border-radius);\n}\n\n\n// Card deck\n\n.card-deck {\n .card {\n margin-bottom: $card-deck-margin;\n }\n\n @include media-breakpoint-up(sm) {\n display: flex;\n flex-flow: row wrap;\n margin-right: -$card-deck-margin;\n margin-left: -$card-deck-margin;\n\n .card {\n // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n flex: 1 0 0%;\n margin-right: $card-deck-margin;\n margin-bottom: 0; // Override the default\n margin-left: $card-deck-margin;\n }\n }\n}\n\n\n//\n// Card groups\n//\n\n.card-group {\n // The child selector allows nested `.card` within `.card-group`\n // to display properly.\n > .card {\n margin-bottom: $card-group-margin;\n }\n\n @include media-breakpoint-up(sm) {\n display: flex;\n flex-flow: row wrap;\n // The child selector allows nested `.card` within `.card-group`\n // to display properly.\n > .card {\n // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4\n flex: 1 0 0%;\n margin-bottom: 0;\n\n + .card {\n margin-left: 0;\n border-left: 0;\n }\n\n // Handle rounded corners\n @if $enable-rounded {\n &:not(:last-child) {\n @include border-right-radius(0);\n\n .card-img-top,\n .card-header {\n // stylelint-disable-next-line property-blacklist\n border-top-right-radius: 0;\n }\n .card-img-bottom,\n .card-footer {\n // stylelint-disable-next-line property-blacklist\n border-bottom-right-radius: 0;\n }\n }\n\n &:not(:first-child) {\n @include border-left-radius(0);\n\n .card-img-top,\n .card-header {\n // stylelint-disable-next-line property-blacklist\n border-top-left-radius: 0;\n }\n .card-img-bottom,\n .card-footer {\n // stylelint-disable-next-line property-blacklist\n border-bottom-left-radius: 0;\n }\n }\n }\n }\n }\n}\n\n\n//\n// Columns\n//\n\n.card-columns {\n .card {\n margin-bottom: $card-columns-margin;\n }\n\n @include media-breakpoint-up(sm) {\n column-count: $card-columns-count;\n column-gap: $card-columns-gap;\n orphans: 1;\n widows: 1;\n\n .card {\n display: inline-block; // Don't let them vertically span multiple columns\n width: 100%; // Don't let their width change\n }\n }\n}\n\n\n//\n// Accordion\n//\n\n.accordion {\n > .card {\n overflow: hidden;\n\n &:not(:last-of-type) {\n border-bottom: 0;\n @include border-bottom-radius(0);\n }\n\n &:not(:first-of-type) {\n @include border-top-radius(0);\n }\n\n > .card-header {\n @include border-radius(0);\n margin-bottom: -$card-border-width;\n }\n }\n}\n",".breadcrumb {\n display: flex;\n flex-wrap: wrap;\n padding: $breadcrumb-padding-y $breadcrumb-padding-x;\n margin-bottom: $breadcrumb-margin-bottom;\n @include font-size($breadcrumb-font-size);\n list-style: none;\n background-color: $breadcrumb-bg;\n @include border-radius($breadcrumb-border-radius);\n}\n\n.breadcrumb-item {\n display: flex;\n\n // The separator between breadcrumbs (by default, a forward-slash: \"/\")\n + .breadcrumb-item {\n padding-left: $breadcrumb-item-padding;\n\n &::before {\n display: inline-block; // Suppress underlining of the separator in modern browsers\n padding-right: $breadcrumb-item-padding;\n color: $breadcrumb-divider-color;\n content: escape-svg($breadcrumb-divider);\n }\n }\n\n // IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built\n // without `<ul>`s. The `::before` pseudo-element generates an element\n // *within* the .breadcrumb-item and thereby inherits the `text-decoration`.\n //\n // To trick IE into suppressing the underline, we give the pseudo-element an\n // underline and then immediately remove it.\n + .breadcrumb-item:hover::before {\n text-decoration: underline;\n }\n // stylelint-disable-next-line no-duplicate-selectors\n + .breadcrumb-item:hover::before {\n text-decoration: none;\n }\n\n &.active {\n color: $breadcrumb-active-color;\n }\n}\n",".pagination {\n display: flex;\n @include list-unstyled();\n @include border-radius();\n}\n\n.page-link {\n position: relative;\n display: block;\n padding: $pagination-padding-y $pagination-padding-x;\n margin-left: -$pagination-border-width;\n line-height: $pagination-line-height;\n color: $pagination-color;\n text-decoration: if($link-decoration == none, null, none);\n background-color: $pagination-bg;\n border: $pagination-border-width solid $pagination-border-color;\n\n &:hover {\n z-index: 2;\n color: $pagination-hover-color;\n text-decoration: none;\n background-color: $pagination-hover-bg;\n border-color: $pagination-hover-border-color;\n }\n\n &:focus {\n z-index: 3;\n outline: $pagination-focus-outline;\n box-shadow: $pagination-focus-box-shadow;\n }\n}\n\n.page-item {\n &:first-child {\n .page-link {\n margin-left: 0;\n @include border-left-radius($border-radius);\n }\n }\n &:last-child {\n .page-link {\n @include border-right-radius($border-radius);\n }\n }\n\n &.active .page-link {\n z-index: 3;\n color: $pagination-active-color;\n background-color: $pagination-active-bg;\n border-color: $pagination-active-border-color;\n }\n\n &.disabled .page-link {\n color: $pagination-disabled-color;\n pointer-events: none;\n // Opinionated: remove the \"hand\" cursor set previously for .page-link\n cursor: auto;\n background-color: $pagination-disabled-bg;\n border-color: $pagination-disabled-border-color;\n }\n}\n\n\n//\n// Sizing\n//\n\n.pagination-lg {\n @include pagination-size($pagination-padding-y-lg, $pagination-padding-x-lg, $font-size-lg, $line-height-lg, $border-radius-lg);\n}\n\n.pagination-sm {\n @include pagination-size($pagination-padding-y-sm, $pagination-padding-x-sm, $font-size-sm, $line-height-sm, $border-radius-sm);\n}\n","// Pagination\n\n@mixin pagination-size($padding-y, $padding-x, $font-size, $line-height, $border-radius) {\n .page-link {\n padding: $padding-y $padding-x;\n @include font-size($font-size);\n line-height: $line-height;\n }\n\n .page-item {\n &:first-child {\n .page-link {\n @include border-left-radius($border-radius);\n }\n }\n &:last-child {\n .page-link {\n @include border-right-radius($border-radius);\n }\n }\n }\n}\n","// Base class\n//\n// Requires one of the contextual, color modifier classes for `color` and\n// `background-color`.\n\n.badge {\n display: inline-block;\n padding: $badge-padding-y $badge-padding-x;\n @include font-size($badge-font-size);\n font-weight: $badge-font-weight;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n @include border-radius($badge-border-radius);\n @include transition($badge-transition);\n\n @at-root a#{&} {\n @include hover-focus() {\n text-decoration: none;\n }\n }\n\n // Empty badges collapse automatically\n &:empty {\n display: none;\n }\n}\n\n// Quick fix for badges in buttons\n.btn .badge {\n position: relative;\n top: -1px;\n}\n\n// Pill badges\n//\n// Make them extra rounded with a modifier to replace v3's badges.\n\n.badge-pill {\n padding-right: $badge-pill-padding-x;\n padding-left: $badge-pill-padding-x;\n @include border-radius($badge-pill-border-radius);\n}\n\n// Colors\n//\n// Contextual variations (linked badges get darker on :hover).\n\n@each $color, $value in $theme-colors {\n .badge-#{$color} {\n @include badge-variant($value);\n }\n}\n","@mixin badge-variant($bg) {\n color: color-yiq($bg);\n background-color: $bg;\n\n @at-root a#{&} {\n @include hover-focus() {\n color: color-yiq($bg);\n background-color: darken($bg, 10%);\n }\n\n &:focus,\n &.focus {\n outline: 0;\n box-shadow: 0 0 0 $badge-focus-width rgba($bg, .5);\n }\n }\n}\n",".jumbotron {\n padding: $jumbotron-padding ($jumbotron-padding / 2);\n margin-bottom: $jumbotron-padding;\n color: $jumbotron-color;\n background-color: $jumbotron-bg;\n @include border-radius($border-radius-lg);\n\n @include media-breakpoint-up(sm) {\n padding: ($jumbotron-padding * 2) $jumbotron-padding;\n }\n}\n\n.jumbotron-fluid {\n padding-right: 0;\n padding-left: 0;\n @include border-radius(0);\n}\n","//\n// Base styles\n//\n\n.alert {\n position: relative;\n padding: $alert-padding-y $alert-padding-x;\n margin-bottom: $alert-margin-bottom;\n border: $alert-border-width solid transparent;\n @include border-radius($alert-border-radius);\n}\n\n// Headings for larger alerts\n.alert-heading {\n // Specified to prevent conflicts of changing $headings-color\n color: inherit;\n}\n\n// Provide class for links that match alerts\n.alert-link {\n font-weight: $alert-link-font-weight;\n}\n\n\n// Dismissible alerts\n//\n// Expand the right padding and account for the close button's positioning.\n\n.alert-dismissible {\n padding-right: $close-font-size + $alert-padding-x * 2;\n\n // Adjust close link position\n .close {\n position: absolute;\n top: 0;\n right: 0;\n padding: $alert-padding-y $alert-padding-x;\n color: inherit;\n }\n}\n\n\n// Alternate styles\n//\n// Generate contextual modifier classes for colorizing the alert.\n\n@each $color, $value in $theme-colors {\n .alert-#{$color} {\n @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level));\n }\n}\n","@mixin alert-variant($background, $border, $color) {\n color: $color;\n @include gradient-bg($background);\n border-color: $border;\n\n hr {\n border-top-color: darken($border, 5%);\n }\n\n .alert-link {\n color: darken($color, 10%);\n }\n}\n","// Disable animation if transitions are disabled\n@if $enable-transitions {\n @keyframes progress-bar-stripes {\n from { background-position: $progress-height 0; }\n to { background-position: 0 0; }\n }\n}\n\n.progress {\n display: flex;\n height: $progress-height;\n overflow: hidden; // force rounded corners by cropping it\n line-height: 0;\n @include font-size($progress-font-size);\n background-color: $progress-bg;\n @include border-radius($progress-border-radius);\n @include box-shadow($progress-box-shadow);\n}\n\n.progress-bar {\n display: flex;\n flex-direction: column;\n justify-content: center;\n overflow: hidden;\n color: $progress-bar-color;\n text-align: center;\n white-space: nowrap;\n background-color: $progress-bar-bg;\n @include transition($progress-bar-transition);\n}\n\n.progress-bar-striped {\n @include gradient-striped();\n background-size: $progress-height $progress-height;\n}\n\n@if $enable-transitions {\n .progress-bar-animated {\n animation: progress-bar-stripes $progress-bar-animation-timing;\n\n @if $enable-prefers-reduced-motion-media-query {\n @media (prefers-reduced-motion: reduce) {\n animation: none;\n }\n }\n }\n}\n",".media {\n display: flex;\n align-items: flex-start;\n}\n\n.media-body {\n flex: 1;\n}\n","// Base class\n//\n// Easily usable on <ul>, <ol>, or <div>.\n\n.list-group {\n display: flex;\n flex-direction: column;\n\n // No need to set list-style: none; since .list-group-item is block level\n padding-left: 0; // reset padding because ul and ol\n margin-bottom: 0;\n @include border-radius($list-group-border-radius);\n}\n\n\n// Interactive list items\n//\n// Use anchor or button elements instead of `li`s or `div`s to create interactive\n// list items. Includes an extra `.active` modifier class for selected items.\n\n.list-group-item-action {\n width: 100%; // For `<button>`s (anchors become 100% by default though)\n color: $list-group-action-color;\n text-align: inherit; // For `<button>`s (anchors inherit)\n\n // Hover state\n @include hover-focus() {\n z-index: 1; // Place hover/focus items above their siblings for proper border styling\n color: $list-group-action-hover-color;\n text-decoration: none;\n background-color: $list-group-hover-bg;\n }\n\n &:active {\n color: $list-group-action-active-color;\n background-color: $list-group-action-active-bg;\n }\n}\n\n\n// Individual list items\n//\n// Use on `li`s or `div`s within the `.list-group` parent.\n\n.list-group-item {\n position: relative;\n display: block;\n padding: $list-group-item-padding-y $list-group-item-padding-x;\n color: $list-group-color;\n text-decoration: if($link-decoration == none, null, none);\n background-color: $list-group-bg;\n border: $list-group-border-width solid $list-group-border-color;\n\n &:first-child {\n @include border-top-radius(inherit);\n }\n\n &:last-child {\n @include border-bottom-radius(inherit);\n }\n\n &.disabled,\n &:disabled {\n color: $list-group-disabled-color;\n pointer-events: none;\n background-color: $list-group-disabled-bg;\n }\n\n // Include both here for `<a>`s and `<button>`s\n &.active {\n z-index: 2; // Place active items above their siblings for proper border styling\n color: $list-group-active-color;\n background-color: $list-group-active-bg;\n border-color: $list-group-active-border-color;\n }\n\n & + & {\n border-top-width: 0;\n\n &.active {\n margin-top: -$list-group-border-width;\n border-top-width: $list-group-border-width;\n }\n }\n}\n\n\n// Horizontal\n//\n// Change the layout of list group items from vertical (default) to horizontal.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .list-group-horizontal#{$infix} {\n flex-direction: row;\n\n > .list-group-item {\n &:first-child {\n @include border-bottom-left-radius($list-group-border-radius);\n @include border-top-right-radius(0);\n }\n\n &:last-child {\n @include border-top-right-radius($list-group-border-radius);\n @include border-bottom-left-radius(0);\n }\n\n &.active {\n margin-top: 0;\n }\n\n & + .list-group-item {\n border-top-width: $list-group-border-width;\n border-left-width: 0;\n\n &.active {\n margin-left: -$list-group-border-width;\n border-left-width: $list-group-border-width;\n }\n }\n }\n }\n }\n}\n\n\n// Flush list items\n//\n// Remove borders and border-radius to keep list group items edge-to-edge. Most\n// useful within other components (e.g., cards).\n\n.list-group-flush {\n @include border-radius(0);\n\n > .list-group-item {\n border-width: 0 0 $list-group-border-width;\n\n &:last-child {\n border-bottom-width: 0;\n }\n }\n}\n\n\n// Contextual variants\n//\n// Add modifier classes to change text and background color on individual items.\n// Organizationally, this must come after the `:hover` states.\n\n@each $color, $value in $theme-colors {\n @include list-group-item-variant($color, theme-color-level($color, -9), theme-color-level($color, 6));\n}\n","// List Groups\n\n@mixin list-group-item-variant($state, $background, $color) {\n .list-group-item-#{$state} {\n color: $color;\n background-color: $background;\n\n &.list-group-item-action {\n @include hover-focus() {\n color: $color;\n background-color: darken($background, 5%);\n }\n\n &.active {\n color: $white;\n background-color: $color;\n border-color: $color;\n }\n }\n }\n}\n",".close {\n float: right;\n @include font-size($close-font-size);\n font-weight: $close-font-weight;\n line-height: 1;\n color: $close-color;\n text-shadow: $close-text-shadow;\n opacity: .5;\n\n // Override <a>'s hover style\n @include hover() {\n color: $close-color;\n text-decoration: none;\n }\n\n &:not(:disabled):not(.disabled) {\n @include hover-focus() {\n opacity: .75;\n }\n }\n}\n\n// Additional properties for button version\n// iOS requires the button element instead of an anchor tag.\n// If you want the anchor version, it requires `href=\"#\"`.\n// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n// stylelint-disable-next-line selector-no-qualifying-type\nbutton.close {\n padding: 0;\n background-color: transparent;\n border: 0;\n}\n\n// Future-proof disabling of clicks on `<a>` elements\n\n// stylelint-disable-next-line selector-no-qualifying-type\na.close.disabled {\n pointer-events: none;\n}\n",".toast {\n max-width: $toast-max-width;\n overflow: hidden; // cheap rounded corners on nested items\n @include font-size($toast-font-size);\n color: $toast-color;\n background-color: $toast-background-color;\n background-clip: padding-box;\n border: $toast-border-width solid $toast-border-color;\n box-shadow: $toast-box-shadow;\n backdrop-filter: blur(10px);\n opacity: 0;\n @include border-radius($toast-border-radius);\n\n &:not(:last-child) {\n margin-bottom: $toast-padding-x;\n }\n\n &.showing {\n opacity: 1;\n }\n\n &.show {\n display: block;\n opacity: 1;\n }\n\n &.hide {\n display: none;\n }\n}\n\n.toast-header {\n display: flex;\n align-items: center;\n padding: $toast-padding-y $toast-padding-x;\n color: $toast-header-color;\n background-color: $toast-header-background-color;\n background-clip: padding-box;\n border-bottom: $toast-border-width solid $toast-header-border-color;\n}\n\n.toast-body {\n padding: $toast-padding-x; // apply to both vertical and horizontal\n}\n","// .modal-open - body class for killing the scroll\n// .modal - container to scroll within\n// .modal-dialog - positioning shell for the actual modal\n// .modal-content - actual modal w/ bg and corners and stuff\n\n\n.modal-open {\n // Kill the scroll on the body\n overflow: hidden;\n\n .modal {\n overflow-x: hidden;\n overflow-y: auto;\n }\n}\n\n// Container that the modal scrolls within\n.modal {\n position: fixed;\n top: 0;\n left: 0;\n z-index: $zindex-modal;\n display: none;\n width: 100%;\n height: 100%;\n overflow: hidden;\n // Prevent Chrome on Windows from adding a focus outline. For details, see\n // https://github.com/twbs/bootstrap/pull/10951.\n outline: 0;\n // We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a\n // gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342\n // See also https://github.com/twbs/bootstrap/issues/17695\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n position: relative;\n width: auto;\n margin: $modal-dialog-margin;\n // allow clicks to pass through for custom click handling to close modal\n pointer-events: none;\n\n // When fading in the modal, animate it to slide down\n .modal.fade & {\n @include transition($modal-transition);\n transform: $modal-fade-transform;\n }\n .modal.show & {\n transform: $modal-show-transform;\n }\n\n // When trying to close, animate focus to scale\n .modal.modal-static & {\n transform: $modal-scale-transform;\n }\n}\n\n.modal-dialog-scrollable {\n display: flex; // IE10/11\n max-height: subtract(100%, $modal-dialog-margin * 2);\n\n .modal-content {\n max-height: subtract(100vh, $modal-dialog-margin * 2); // IE10/11\n overflow: hidden;\n }\n\n .modal-header,\n .modal-footer {\n flex-shrink: 0;\n }\n\n .modal-body {\n overflow-y: auto;\n }\n}\n\n.modal-dialog-centered {\n display: flex;\n align-items: center;\n min-height: subtract(100%, $modal-dialog-margin * 2);\n\n // Ensure `modal-dialog-centered` extends the full height of the view (IE10/11)\n &::before {\n display: block; // IE10\n height: subtract(100vh, $modal-dialog-margin * 2);\n height: min-content; // Reset height to 0 except on IE\n content: \"\";\n }\n\n // Ensure `.modal-body` shows scrollbar (IE10/11)\n &.modal-dialog-scrollable {\n flex-direction: column;\n justify-content: center;\n height: 100%;\n\n .modal-content {\n max-height: none;\n }\n\n &::before {\n content: none;\n }\n }\n}\n\n// Actual modal\n.modal-content {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`\n // counteract the pointer-events: none; in the .modal-dialog\n color: $modal-content-color;\n pointer-events: auto;\n background-color: $modal-content-bg;\n background-clip: padding-box;\n border: $modal-content-border-width solid $modal-content-border-color;\n @include border-radius($modal-content-border-radius);\n @include box-shadow($modal-content-box-shadow-xs);\n // Remove focus outline from opened modal\n outline: 0;\n}\n\n// Modal background\n.modal-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n z-index: $zindex-modal-backdrop;\n width: 100vw;\n height: 100vh;\n background-color: $modal-backdrop-bg;\n\n // Fade for backdrop\n &.fade { opacity: 0; }\n &.show { opacity: $modal-backdrop-opacity; }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n display: flex;\n align-items: flex-start; // so the close btn always stays on the upper right corner\n justify-content: space-between; // Put modal header elements (title and dismiss) on opposite ends\n padding: $modal-header-padding;\n border-bottom: $modal-header-border-width solid $modal-header-border-color;\n @include border-top-radius($modal-content-inner-border-radius);\n\n .close {\n padding: $modal-header-padding;\n // auto on the left force icon to the right even when there is no .modal-title\n margin: (-$modal-header-padding-y) (-$modal-header-padding-x) (-$modal-header-padding-y) auto;\n }\n}\n\n// Title text within header\n.modal-title {\n margin-bottom: 0;\n line-height: $modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n position: relative;\n // Enable `flex-grow: 1` so that the body take up as much space as possible\n // when there should be a fixed height on `.modal-dialog`.\n flex: 1 1 auto;\n padding: $modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n display: flex;\n flex-wrap: wrap;\n align-items: center; // vertically center\n justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items\n padding: $modal-inner-padding - $modal-footer-margin-between / 2;\n border-top: $modal-footer-border-width solid $modal-footer-border-color;\n @include border-bottom-radius($modal-content-inner-border-radius);\n\n // Place margin between footer elements\n // This solution is far from ideal because of the universal selector usage,\n // but is needed to fix https://github.com/twbs/bootstrap/issues/24800\n // stylelint-disable-next-line selector-max-universal\n > * {\n margin: $modal-footer-margin-between / 2;\n }\n}\n\n// Measure scrollbar width for padding body during modal show/hide\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n// Scale up the modal\n@include media-breakpoint-up(sm) {\n // Automatically set modal's width for larger viewports\n .modal-dialog {\n max-width: $modal-md;\n margin: $modal-dialog-margin-y-sm-up auto;\n }\n\n .modal-dialog-scrollable {\n max-height: subtract(100%, $modal-dialog-margin-y-sm-up * 2);\n\n .modal-content {\n max-height: subtract(100vh, $modal-dialog-margin-y-sm-up * 2);\n }\n }\n\n .modal-dialog-centered {\n min-height: subtract(100%, $modal-dialog-margin-y-sm-up * 2);\n\n &::before {\n height: subtract(100vh, $modal-dialog-margin-y-sm-up * 2);\n height: min-content;\n }\n }\n\n .modal-content {\n @include box-shadow($modal-content-box-shadow-sm-up);\n }\n\n .modal-sm { max-width: $modal-sm; }\n}\n\n@include media-breakpoint-up(lg) {\n .modal-lg,\n .modal-xl {\n max-width: $modal-lg;\n }\n}\n\n@include media-breakpoint-up(xl) {\n .modal-xl { max-width: $modal-xl; }\n}\n","// Base class\n.tooltip {\n position: absolute;\n z-index: $zindex-tooltip;\n display: block;\n margin: $tooltip-margin;\n // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n @include reset-text();\n @include font-size($tooltip-font-size);\n // Allow breaking very long words so they don't overflow the tooltip's bounds\n word-wrap: break-word;\n opacity: 0;\n\n &.show { opacity: $tooltip-opacity; }\n\n .arrow {\n position: absolute;\n display: block;\n width: $tooltip-arrow-width;\n height: $tooltip-arrow-height;\n\n &::before {\n position: absolute;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n }\n }\n}\n\n.bs-tooltip-top {\n padding: $tooltip-arrow-height 0;\n\n .arrow {\n bottom: 0;\n\n &::before {\n top: 0;\n border-width: $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;\n border-top-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-right {\n padding: 0 $tooltip-arrow-height;\n\n .arrow {\n left: 0;\n width: $tooltip-arrow-height;\n height: $tooltip-arrow-width;\n\n &::before {\n right: 0;\n border-width: ($tooltip-arrow-width / 2) $tooltip-arrow-height ($tooltip-arrow-width / 2) 0;\n border-right-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-bottom {\n padding: $tooltip-arrow-height 0;\n\n .arrow {\n top: 0;\n\n &::before {\n bottom: 0;\n border-width: 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;\n border-bottom-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-left {\n padding: 0 $tooltip-arrow-height;\n\n .arrow {\n right: 0;\n width: $tooltip-arrow-height;\n height: $tooltip-arrow-width;\n\n &::before {\n left: 0;\n border-width: ($tooltip-arrow-width / 2) 0 ($tooltip-arrow-width / 2) $tooltip-arrow-height;\n border-left-color: $tooltip-arrow-color;\n }\n }\n}\n\n.bs-tooltip-auto {\n &[x-placement^=\"top\"] {\n @extend .bs-tooltip-top;\n }\n &[x-placement^=\"right\"] {\n @extend .bs-tooltip-right;\n }\n &[x-placement^=\"bottom\"] {\n @extend .bs-tooltip-bottom;\n }\n &[x-placement^=\"left\"] {\n @extend .bs-tooltip-left;\n }\n}\n\n// Wrapper for the tooltip content\n.tooltip-inner {\n max-width: $tooltip-max-width;\n padding: $tooltip-padding-y $tooltip-padding-x;\n color: $tooltip-color;\n text-align: center;\n background-color: $tooltip-bg;\n @include border-radius($tooltip-border-radius);\n}\n","@mixin reset-text() {\n font-family: $font-family-base;\n // We deliberately do NOT reset font-size or word-wrap.\n font-style: normal;\n font-weight: $font-weight-normal;\n line-height: $line-height-base;\n text-align: left; // Fallback for where `start` is not supported\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n white-space: normal;\n line-break: auto;\n}\n",".popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: $zindex-popover;\n display: block;\n max-width: $popover-max-width;\n // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n @include reset-text();\n @include font-size($popover-font-size);\n // Allow breaking very long words so they don't overflow the popover's bounds\n word-wrap: break-word;\n background-color: $popover-bg;\n background-clip: padding-box;\n border: $popover-border-width solid $popover-border-color;\n @include border-radius($popover-border-radius);\n @include box-shadow($popover-box-shadow);\n\n .arrow {\n position: absolute;\n display: block;\n width: $popover-arrow-width;\n height: $popover-arrow-height;\n margin: 0 $popover-border-radius;\n\n &::before,\n &::after {\n position: absolute;\n display: block;\n content: \"\";\n border-color: transparent;\n border-style: solid;\n }\n }\n}\n\n.bs-popover-top {\n margin-bottom: $popover-arrow-height;\n\n > .arrow {\n bottom: subtract(-$popover-arrow-height, $popover-border-width);\n\n &::before {\n bottom: 0;\n border-width: $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-top-color: $popover-arrow-outer-color;\n }\n\n &::after {\n bottom: $popover-border-width;\n border-width: $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-top-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-right {\n margin-left: $popover-arrow-height;\n\n > .arrow {\n left: subtract(-$popover-arrow-height, $popover-border-width);\n width: $popover-arrow-height;\n height: $popover-arrow-width;\n margin: $popover-border-radius 0; // make sure the arrow does not touch the popover's rounded corners\n\n &::before {\n left: 0;\n border-width: ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-right-color: $popover-arrow-outer-color;\n }\n\n &::after {\n left: $popover-border-width;\n border-width: ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2) 0;\n border-right-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-bottom {\n margin-top: $popover-arrow-height;\n\n > .arrow {\n top: subtract(-$popover-arrow-height, $popover-border-width);\n\n &::before {\n top: 0;\n border-width: 0 ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2);\n border-bottom-color: $popover-arrow-outer-color;\n }\n\n &::after {\n top: $popover-border-width;\n border-width: 0 ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2);\n border-bottom-color: $popover-arrow-color;\n }\n }\n\n // This will remove the popover-header's border just below the arrow\n .popover-header::before {\n position: absolute;\n top: 0;\n left: 50%;\n display: block;\n width: $popover-arrow-width;\n margin-left: -$popover-arrow-width / 2;\n content: \"\";\n border-bottom: $popover-border-width solid $popover-header-bg;\n }\n}\n\n.bs-popover-left {\n margin-right: $popover-arrow-height;\n\n > .arrow {\n right: subtract(-$popover-arrow-height, $popover-border-width);\n width: $popover-arrow-height;\n height: $popover-arrow-width;\n margin: $popover-border-radius 0; // make sure the arrow does not touch the popover's rounded corners\n\n &::before {\n right: 0;\n border-width: ($popover-arrow-width / 2) 0 ($popover-arrow-width / 2) $popover-arrow-height;\n border-left-color: $popover-arrow-outer-color;\n }\n\n &::after {\n right: $popover-border-width;\n border-width: ($popover-arrow-width / 2) 0 ($popover-arrow-width / 2) $popover-arrow-height;\n border-left-color: $popover-arrow-color;\n }\n }\n}\n\n.bs-popover-auto {\n &[x-placement^=\"top\"] {\n @extend .bs-popover-top;\n }\n &[x-placement^=\"right\"] {\n @extend .bs-popover-right;\n }\n &[x-placement^=\"bottom\"] {\n @extend .bs-popover-bottom;\n }\n &[x-placement^=\"left\"] {\n @extend .bs-popover-left;\n }\n}\n\n\n// Offset the popover to account for the popover arrow\n.popover-header {\n padding: $popover-header-padding-y $popover-header-padding-x;\n margin-bottom: 0; // Reset the default from Reboot\n @include font-size($font-size-base);\n color: $popover-header-color;\n background-color: $popover-header-bg;\n border-bottom: $popover-border-width solid darken($popover-header-bg, 5%);\n @include border-top-radius($popover-inner-border-radius);\n\n &:empty {\n display: none;\n }\n}\n\n.popover-body {\n padding: $popover-body-padding-y $popover-body-padding-x;\n color: $popover-body-color;\n}\n","// Notes on the classes:\n//\n// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)\n// even when their scroll action started on a carousel, but for compatibility (with Firefox)\n// we're preventing all actions instead\n// 2. The .carousel-item-left and .carousel-item-right is used to indicate where\n// the active slide is heading.\n// 3. .active.carousel-item is the current slide.\n// 4. .active.carousel-item-left and .active.carousel-item-right is the current\n// slide in its in-transition state. Only one of these occurs at a time.\n// 5. .carousel-item-next.carousel-item-left and .carousel-item-prev.carousel-item-right\n// is the upcoming slide in transition.\n\n.carousel {\n position: relative;\n}\n\n.carousel.pointer-event {\n touch-action: pan-y;\n}\n\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n @include clearfix();\n}\n\n.carousel-item {\n position: relative;\n display: none;\n float: left;\n width: 100%;\n margin-right: -100%;\n backface-visibility: hidden;\n @include transition($carousel-transition);\n}\n\n.carousel-item.active,\n.carousel-item-next,\n.carousel-item-prev {\n display: block;\n}\n\n.carousel-item-next:not(.carousel-item-left),\n.active.carousel-item-right {\n transform: translateX(100%);\n}\n\n.carousel-item-prev:not(.carousel-item-right),\n.active.carousel-item-left {\n transform: translateX(-100%);\n}\n\n\n//\n// Alternate transitions\n//\n\n.carousel-fade {\n .carousel-item {\n opacity: 0;\n transition-property: opacity;\n transform: none;\n }\n\n .carousel-item.active,\n .carousel-item-next.carousel-item-left,\n .carousel-item-prev.carousel-item-right {\n z-index: 1;\n opacity: 1;\n }\n\n .active.carousel-item-left,\n .active.carousel-item-right {\n z-index: 0;\n opacity: 0;\n @include transition(opacity 0s $carousel-transition-duration);\n }\n}\n\n\n//\n// Left/right controls for nav\n//\n\n.carousel-control-prev,\n.carousel-control-next {\n position: absolute;\n top: 0;\n bottom: 0;\n z-index: 1;\n // Use flex for alignment (1-3)\n display: flex; // 1. allow flex styles\n align-items: center; // 2. vertically center contents\n justify-content: center; // 3. horizontally center contents\n width: $carousel-control-width;\n color: $carousel-control-color;\n text-align: center;\n opacity: $carousel-control-opacity;\n @include transition($carousel-control-transition);\n\n // Hover/focus state\n @include hover-focus() {\n color: $carousel-control-color;\n text-decoration: none;\n outline: 0;\n opacity: $carousel-control-hover-opacity;\n }\n}\n.carousel-control-prev {\n left: 0;\n @if $enable-gradients {\n background-image: linear-gradient(90deg, rgba($black, .25), rgba($black, .001));\n }\n}\n.carousel-control-next {\n right: 0;\n @if $enable-gradients {\n background-image: linear-gradient(270deg, rgba($black, .25), rgba($black, .001));\n }\n}\n\n// Icons for within\n.carousel-control-prev-icon,\n.carousel-control-next-icon {\n display: inline-block;\n width: $carousel-control-icon-width;\n height: $carousel-control-icon-width;\n background: no-repeat 50% / 100% 100%;\n}\n.carousel-control-prev-icon {\n background-image: escape-svg($carousel-control-prev-icon-bg);\n}\n.carousel-control-next-icon {\n background-image: escape-svg($carousel-control-next-icon-bg);\n}\n\n\n// Optional indicator pips\n//\n// Add an ordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 15;\n display: flex;\n justify-content: center;\n padding-left: 0; // override <ol> default\n // Use the .carousel-control's width as margin so we don't overlay those\n margin-right: $carousel-control-width;\n margin-left: $carousel-control-width;\n list-style: none;\n\n li {\n box-sizing: content-box;\n flex: 0 1 auto;\n width: $carousel-indicator-width;\n height: $carousel-indicator-height;\n margin-right: $carousel-indicator-spacer;\n margin-left: $carousel-indicator-spacer;\n text-indent: -999px;\n cursor: pointer;\n background-color: $carousel-indicator-active-bg;\n background-clip: padding-box;\n // Use transparent borders to increase the hit area by 10px on top and bottom.\n border-top: $carousel-indicator-hit-area-height solid transparent;\n border-bottom: $carousel-indicator-hit-area-height solid transparent;\n opacity: .5;\n @include transition($carousel-indicator-transition);\n }\n\n .active {\n opacity: 1;\n }\n}\n\n\n// Optional captions\n//\n//\n\n.carousel-caption {\n position: absolute;\n right: (100% - $carousel-caption-width) / 2;\n bottom: 20px;\n left: (100% - $carousel-caption-width) / 2;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: $carousel-caption-color;\n text-align: center;\n}\n","@mixin clearfix() {\n &::after {\n display: block;\n clear: both;\n content: \"\";\n }\n}\n","//\n// Rotating border\n//\n\n@keyframes spinner-border {\n to { transform: rotate(360deg); }\n}\n\n.spinner-border {\n display: inline-block;\n width: $spinner-width;\n height: $spinner-height;\n vertical-align: text-bottom;\n border: $spinner-border-width solid currentColor;\n border-right-color: transparent;\n // stylelint-disable-next-line property-blacklist\n border-radius: 50%;\n animation: spinner-border .75s linear infinite;\n}\n\n.spinner-border-sm {\n width: $spinner-width-sm;\n height: $spinner-height-sm;\n border-width: $spinner-border-width-sm;\n}\n\n//\n// Growing circle\n//\n\n@keyframes spinner-grow {\n 0% {\n transform: scale(0);\n }\n 50% {\n opacity: 1;\n transform: none;\n }\n}\n\n.spinner-grow {\n display: inline-block;\n width: $spinner-width;\n height: $spinner-height;\n vertical-align: text-bottom;\n background-color: currentColor;\n // stylelint-disable-next-line property-blacklist\n border-radius: 50%;\n opacity: 0;\n animation: spinner-grow .75s linear infinite;\n}\n\n.spinner-grow-sm {\n width: $spinner-width-sm;\n height: $spinner-height-sm;\n}\n","// stylelint-disable declaration-no-important\n\n.align-baseline { vertical-align: baseline !important; } // Browser default\n.align-top { vertical-align: top !important; }\n.align-middle { vertical-align: middle !important; }\n.align-bottom { vertical-align: bottom !important; }\n.align-text-bottom { vertical-align: text-bottom !important; }\n.align-text-top { vertical-align: text-top !important; }\n","// stylelint-disable declaration-no-important\n\n// Contextual backgrounds\n\n@mixin bg-variant($parent, $color, $ignore-warning: false) {\n #{$parent} {\n background-color: $color !important;\n }\n a#{$parent},\n button#{$parent} {\n @include hover-focus() {\n background-color: darken($color, 10%) !important;\n }\n }\n @include deprecate(\"The `bg-variant` mixin\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n\n@mixin bg-gradient-variant($parent, $color, $ignore-warning: false) {\n #{$parent} {\n background: $color linear-gradient(180deg, mix($body-bg, $color, 15%), $color) repeat-x !important;\n }\n @include deprecate(\"The `bg-gradient-variant` mixin\", \"v4.5.0\", \"v5\", $ignore-warning);\n}\n","// stylelint-disable declaration-no-important\n\n@each $color, $value in $theme-colors {\n @include bg-variant(\".bg-#{$color}\", $value, true);\n}\n\n@if $enable-gradients {\n @each $color, $value in $theme-colors {\n @include bg-gradient-variant(\".bg-gradient-#{$color}\", $value, true);\n }\n}\n\n.bg-white {\n background-color: $white !important;\n}\n\n.bg-transparent {\n background-color: transparent !important;\n}\n","// stylelint-disable property-blacklist, declaration-no-important\n\n//\n// Border\n//\n\n.border { border: $border-width solid $border-color !important; }\n.border-top { border-top: $border-width solid $border-color !important; }\n.border-right { border-right: $border-width solid $border-color !important; }\n.border-bottom { border-bottom: $border-width solid $border-color !important; }\n.border-left { border-left: $border-width solid $border-color !important; }\n\n.border-0 { border: 0 !important; }\n.border-top-0 { border-top: 0 !important; }\n.border-right-0 { border-right: 0 !important; }\n.border-bottom-0 { border-bottom: 0 !important; }\n.border-left-0 { border-left: 0 !important; }\n\n@each $color, $value in $theme-colors {\n .border-#{$color} {\n border-color: $value !important;\n }\n}\n\n.border-white {\n border-color: $white !important;\n}\n\n//\n// Border-radius\n//\n\n.rounded-sm {\n border-radius: $border-radius-sm !important;\n}\n\n.rounded {\n border-radius: $border-radius !important;\n}\n\n.rounded-top {\n border-top-left-radius: $border-radius !important;\n border-top-right-radius: $border-radius !important;\n}\n\n.rounded-right {\n border-top-right-radius: $border-radius !important;\n border-bottom-right-radius: $border-radius !important;\n}\n\n.rounded-bottom {\n border-bottom-right-radius: $border-radius !important;\n border-bottom-left-radius: $border-radius !important;\n}\n\n.rounded-left {\n border-top-left-radius: $border-radius !important;\n border-bottom-left-radius: $border-radius !important;\n}\n\n.rounded-lg {\n border-radius: $border-radius-lg !important;\n}\n\n.rounded-circle {\n border-radius: 50% !important;\n}\n\n.rounded-pill {\n border-radius: $rounded-pill !important;\n}\n\n.rounded-0 {\n border-radius: 0 !important;\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Utilities for common `display` values\n//\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $value in $displays {\n .d#{$infix}-#{$value} { display: $value !important; }\n }\n }\n}\n\n\n//\n// Utilities for toggling `display` in print\n//\n\n@media print {\n @each $value in $displays {\n .d-print-#{$value} { display: $value !important; }\n }\n}\n","// Credit: Nicolas Gallagher and SUIT CSS.\n\n.embed-responsive {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n\n &::before {\n display: block;\n content: \"\";\n }\n\n .embed-responsive-item,\n iframe,\n embed,\n object,\n video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n }\n}\n\n@each $embed-responsive-aspect-ratio in $embed-responsive-aspect-ratios {\n $embed-responsive-aspect-ratio-x: nth($embed-responsive-aspect-ratio, 1);\n $embed-responsive-aspect-ratio-y: nth($embed-responsive-aspect-ratio, 2);\n\n .embed-responsive-#{$embed-responsive-aspect-ratio-x}by#{$embed-responsive-aspect-ratio-y} {\n &::before {\n padding-top: percentage($embed-responsive-aspect-ratio-y / $embed-responsive-aspect-ratio-x);\n }\n }\n}\n","// stylelint-disable declaration-no-important\n\n// Flex variation\n//\n// Custom styles for additional flex alignment options.\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .flex#{$infix}-row { flex-direction: row !important; }\n .flex#{$infix}-column { flex-direction: column !important; }\n .flex#{$infix}-row-reverse { flex-direction: row-reverse !important; }\n .flex#{$infix}-column-reverse { flex-direction: column-reverse !important; }\n\n .flex#{$infix}-wrap { flex-wrap: wrap !important; }\n .flex#{$infix}-nowrap { flex-wrap: nowrap !important; }\n .flex#{$infix}-wrap-reverse { flex-wrap: wrap-reverse !important; }\n .flex#{$infix}-fill { flex: 1 1 auto !important; }\n .flex#{$infix}-grow-0 { flex-grow: 0 !important; }\n .flex#{$infix}-grow-1 { flex-grow: 1 !important; }\n .flex#{$infix}-shrink-0 { flex-shrink: 0 !important; }\n .flex#{$infix}-shrink-1 { flex-shrink: 1 !important; }\n\n .justify-content#{$infix}-start { justify-content: flex-start !important; }\n .justify-content#{$infix}-end { justify-content: flex-end !important; }\n .justify-content#{$infix}-center { justify-content: center !important; }\n .justify-content#{$infix}-between { justify-content: space-between !important; }\n .justify-content#{$infix}-around { justify-content: space-around !important; }\n\n .align-items#{$infix}-start { align-items: flex-start !important; }\n .align-items#{$infix}-end { align-items: flex-end !important; }\n .align-items#{$infix}-center { align-items: center !important; }\n .align-items#{$infix}-baseline { align-items: baseline !important; }\n .align-items#{$infix}-stretch { align-items: stretch !important; }\n\n .align-content#{$infix}-start { align-content: flex-start !important; }\n .align-content#{$infix}-end { align-content: flex-end !important; }\n .align-content#{$infix}-center { align-content: center !important; }\n .align-content#{$infix}-between { align-content: space-between !important; }\n .align-content#{$infix}-around { align-content: space-around !important; }\n .align-content#{$infix}-stretch { align-content: stretch !important; }\n\n .align-self#{$infix}-auto { align-self: auto !important; }\n .align-self#{$infix}-start { align-self: flex-start !important; }\n .align-self#{$infix}-end { align-self: flex-end !important; }\n .align-self#{$infix}-center { align-self: center !important; }\n .align-self#{$infix}-baseline { align-self: baseline !important; }\n .align-self#{$infix}-stretch { align-self: stretch !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .float#{$infix}-left { float: left !important; }\n .float#{$infix}-right { float: right !important; }\n .float#{$infix}-none { float: none !important; }\n }\n}\n","// stylelint-disable declaration-no-important\n\n@each $value in $user-selects {\n .user-select-#{$value} { user-select: $value !important; }\n}\n","// stylelint-disable declaration-no-important\n\n@each $value in $overflows {\n .overflow-#{$value} { overflow: $value !important; }\n}\n","// stylelint-disable declaration-no-important\n\n// Common values\n@each $position in $positions {\n .position-#{$position} { position: $position !important; }\n}\n\n// Shorthand\n\n.fixed-top {\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n z-index: $zindex-fixed;\n}\n\n.fixed-bottom {\n position: fixed;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: $zindex-fixed;\n}\n\n.sticky-top {\n @supports (position: sticky) {\n position: sticky;\n top: 0;\n z-index: $zindex-sticky;\n }\n}\n","//\n// Screenreaders\n//\n\n.sr-only {\n @include sr-only();\n}\n\n.sr-only-focusable {\n @include sr-only-focusable();\n}\n","// Only display content to screen readers\n//\n// See: https://a11yproject.com/posts/how-to-hide-content/\n// See: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/\n\n@mixin sr-only() {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px; // Fix for https://github.com/twbs/bootstrap/issues/25686\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n//\n// Useful for \"Skip to main content\" links; see https://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n//\n// Credit: HTML5 Boilerplate\n\n@mixin sr-only-focusable() {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n overflow: visible;\n clip: auto;\n white-space: normal;\n }\n}\n","// stylelint-disable declaration-no-important\n\n.shadow-sm { box-shadow: $box-shadow-sm !important; }\n.shadow { box-shadow: $box-shadow !important; }\n.shadow-lg { box-shadow: $box-shadow-lg !important; }\n.shadow-none { box-shadow: none !important; }\n","// stylelint-disable declaration-no-important\n\n// Width and height\n\n@each $prop, $abbrev in (width: w, height: h) {\n @each $size, $length in $sizes {\n .#{$abbrev}-#{$size} { #{$prop}: $length !important; }\n }\n}\n\n.mw-100 { max-width: 100% !important; }\n.mh-100 { max-height: 100% !important; }\n\n// Viewport additional helpers\n\n.min-vw-100 { min-width: 100vw !important; }\n.min-vh-100 { min-height: 100vh !important; }\n\n.vw-100 { width: 100vw !important; }\n.vh-100 { height: 100vh !important; }\n","// stylelint-disable declaration-no-important\n\n// Margin and Padding\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n @each $prop, $abbrev in (margin: m, padding: p) {\n @each $size, $length in $spacers {\n .#{$abbrev}#{$infix}-#{$size} { #{$prop}: $length !important; }\n .#{$abbrev}t#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-top: $length !important;\n }\n .#{$abbrev}r#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-right: $length !important;\n }\n .#{$abbrev}b#{$infix}-#{$size},\n .#{$abbrev}y#{$infix}-#{$size} {\n #{$prop}-bottom: $length !important;\n }\n .#{$abbrev}l#{$infix}-#{$size},\n .#{$abbrev}x#{$infix}-#{$size} {\n #{$prop}-left: $length !important;\n }\n }\n }\n\n // Negative margins (e.g., where `.mb-n1` is negative version of `.mb-1`)\n @each $size, $length in $spacers {\n @if $size != 0 {\n .m#{$infix}-n#{$size} { margin: -$length !important; }\n .mt#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-top: -$length !important;\n }\n .mr#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-right: -$length !important;\n }\n .mb#{$infix}-n#{$size},\n .my#{$infix}-n#{$size} {\n margin-bottom: -$length !important;\n }\n .ml#{$infix}-n#{$size},\n .mx#{$infix}-n#{$size} {\n margin-left: -$length !important;\n }\n }\n }\n\n // Some special margin utils\n .m#{$infix}-auto { margin: auto !important; }\n .mt#{$infix}-auto,\n .my#{$infix}-auto {\n margin-top: auto !important;\n }\n .mr#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-right: auto !important;\n }\n .mb#{$infix}-auto,\n .my#{$infix}-auto {\n margin-bottom: auto !important;\n }\n .ml#{$infix}-auto,\n .mx#{$infix}-auto {\n margin-left: auto !important;\n }\n }\n}\n","//\n// Stretched link\n//\n\n.stretched-link {\n &::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n // Just in case `pointer-events: none` is set on a parent\n pointer-events: auto;\n content: \"\";\n // IE10 bugfix, see https://stackoverflow.com/questions/16947967/ie10-hover-pseudo-class-doesnt-work-without-background-color\n background-color: rgba(0, 0, 0, 0);\n }\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Text\n//\n\n.text-monospace { font-family: $font-family-monospace !important; }\n\n// Alignment\n\n.text-justify { text-align: justify !important; }\n.text-wrap { white-space: normal !important; }\n.text-nowrap { white-space: nowrap !important; }\n.text-truncate { @include text-truncate(); }\n\n// Responsive alignment\n\n@each $breakpoint in map-keys($grid-breakpoints) {\n @include media-breakpoint-up($breakpoint) {\n $infix: breakpoint-infix($breakpoint, $grid-breakpoints);\n\n .text#{$infix}-left { text-align: left !important; }\n .text#{$infix}-right { text-align: right !important; }\n .text#{$infix}-center { text-align: center !important; }\n }\n}\n\n// Transformation\n\n.text-lowercase { text-transform: lowercase !important; }\n.text-uppercase { text-transform: uppercase !important; }\n.text-capitalize { text-transform: capitalize !important; }\n\n// Weight and italics\n\n.font-weight-light { font-weight: $font-weight-light !important; }\n.font-weight-lighter { font-weight: $font-weight-lighter !important; }\n.font-weight-normal { font-weight: $font-weight-normal !important; }\n.font-weight-bold { font-weight: $font-weight-bold !important; }\n.font-weight-bolder { font-weight: $font-weight-bolder !important; }\n.font-italic { font-style: italic !important; }\n\n// Contextual colors\n\n.text-white { color: $white !important; }\n\n@each $color, $value in $theme-colors {\n @include text-emphasis-variant(\".text-#{$color}\", $value, true);\n}\n\n.text-body { color: $body-color !important; }\n.text-muted { color: $text-muted !important; }\n\n.text-black-50 { color: rgba($black, .5) !important; }\n.text-white-50 { color: rgba($white, .5) !important; }\n\n// Misc\n\n.text-hide {\n @include text-hide($ignore-warning: true);\n}\n\n.text-decoration-none { text-decoration: none !important; }\n\n.text-break {\n word-wrap: break-word !important;\n}\n\n// Reset\n\n.text-reset { color: inherit !important; }\n","// Text truncate\n// Requires inline-block or block for proper styling\n\n@mixin text-truncate() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","// stylelint-disable declaration-no-important\n\n// Typography\n\n@mixin text-emphasis-variant($parent, $color, $ignore-warning: false) {\n #{$parent} {\n color: $color !important;\n }\n @if $emphasized-link-hover-darken-percentage != 0 {\n a#{$parent} {\n @include hover-focus() {\n color: darken($color, $emphasized-link-hover-darken-percentage) !important;\n }\n }\n }\n @include deprecate(\"`text-emphasis-variant()`\", \"v4.4.0\", \"v5\", $ignore-warning);\n}\n","// CSS image replacement\n@mixin text-hide($ignore-warning: false) {\n // stylelint-disable-next-line font-family-no-missing-generic-family-keyword\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n\n @include deprecate(\"`text-hide()`\", \"v4.1.0\", \"v5\", $ignore-warning);\n}\n","// stylelint-disable declaration-no-important\n\n//\n// Visibility utilities\n//\n\n.visible {\n visibility: visible !important;\n}\n\n.invisible {\n visibility: hidden !important;\n}\n","// stylelint-disable declaration-no-important, selector-no-qualifying-type\n\n// Source: https://github.com/h5bp/main.css/blob/master/src/_print.css\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request:\n// https://www.phpied.com/delay-loading-your-print-css/\n// ==========================================================================\n\n@if $enable-print-styles {\n @media print {\n *,\n *::before,\n *::after {\n // Bootstrap specific; comment out `color` and `background`\n //color: $black !important; // Black prints faster\n text-shadow: none !important;\n //background: transparent !important;\n box-shadow: none !important;\n }\n\n a {\n &:not(.btn) {\n text-decoration: underline;\n }\n }\n\n // Bootstrap specific; comment the following selector out\n //a[href]::after {\n // content: \" (\" attr(href) \")\";\n //}\n\n abbr[title]::after {\n content: \" (\" attr(title) \")\";\n }\n\n // Bootstrap specific; comment the following selector out\n //\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n //\n\n //a[href^=\"#\"]::after,\n //a[href^=\"javascript:\"]::after {\n // content: \"\";\n //}\n\n pre {\n white-space: pre-wrap !important;\n }\n pre,\n blockquote {\n border: $border-width solid $gray-500; // Bootstrap custom code; using `$border-width` instead of 1px\n page-break-inside: avoid;\n }\n\n //\n // Printing Tables:\n // https://web.archive.org/web/20180815150934/http://css-discuss.incutio.com/wiki/Printing_Tables\n //\n\n thead {\n display: table-header-group;\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Specify a size and min-width to make printing closer across browsers.\n // We don't set margin here because it breaks `size` in Chrome. We also\n // don't use `!important` on `size` as it breaks in Chrome.\n @page {\n size: $print-page-size;\n }\n body {\n min-width: $print-body-min-width !important;\n }\n .container {\n min-width: $print-body-min-width !important;\n }\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .badge {\n border: $border-width solid $black;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: $white !important;\n }\n }\n\n .table-bordered {\n th,\n td {\n border: 1px solid $gray-300 !important;\n }\n }\n\n .table-dark {\n color: inherit;\n\n th,\n td,\n thead th,\n tbody + tbody {\n border-color: $table-border-color;\n }\n }\n\n .table .thead-dark th {\n color: inherit;\n border-color: $table-border-color;\n }\n\n // Bootstrap specific changes end\n }\n}\n"]}
@@ -1,7 +0,0 @@-/*!- * Bootstrap v4.5.0 (https://getbootstrap.com/)- * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)- */-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,(function(t,e,n){"use strict";function i(t,e){for(var n=0;n<e.length;n++){var i=e[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(t,i.key,i)}}function o(t,e,n){return e&&i(t.prototype,e),n&&i(t,n),t}function s(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function r(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function a(t){for(var e=1;e<arguments.length;e++){var n=null!=arguments[e]?arguments[e]:{};e%2?r(Object(n),!0).forEach((function(e){s(t,e,n[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):r(Object(n)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))}))}return t}e=e&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e,n=n&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n;function l(t){var n=this,i=!1;return e(this).one(c.TRANSITION_END,(function(){i=!0})),setTimeout((function(){i||c.triggerTransitionEnd(n)}),t),this}var c={TRANSITION_END:"bsTransitionEnd",getUID:function(t){do{t+=~~(1e6*Math.random())}while(document.getElementById(t));return t},getSelectorFromElement:function(t){var e=t.getAttribute("data-target");if(!e||"#"===e){var n=t.getAttribute("href");e=n&&"#"!==n?n.trim():""}try{return document.querySelector(e)?e:null}catch(t){return null}},getTransitionDurationFromElement:function(t){if(!t)return 0;var n=e(t).css("transition-duration"),i=e(t).css("transition-delay"),o=parseFloat(n),s=parseFloat(i);return o||s?(n=n.split(",")[0],i=i.split(",")[0],1e3*(parseFloat(n)+parseFloat(i))):0},reflow:function(t){return t.offsetHeight},triggerTransitionEnd:function(t){e(t).trigger("transitionend")},supportsTransitionEnd:function(){return Boolean("transitionend")},isElement:function(t){return(t[0]||t).nodeType},typeCheckConfig:function(t,e,n){for(var i in n)if(Object.prototype.hasOwnProperty.call(n,i)){var o=n[i],s=e[i],r=s&&c.isElement(s)?"element":null===(a=s)||"undefined"==typeof a?""+a:{}.toString.call(a).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(o).test(r))throw new Error(t.toUpperCase()+': Option "'+i+'" provided type "'+r+'" but expected type "'+o+'".')}var a},findShadowRoot:function(t){if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){var e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c.findShadowRoot(t.parentNode):null},jQueryDetection:function(){if("undefined"==typeof e)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var t=e.fn.jquery.split(" ")[0].split(".");if(t[0]<2&&t[1]<9||1===t[0]&&9===t[1]&&t[2]<1||t[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};c.jQueryDetection(),e.fn.emulateTransitionEnd=l,e.event.special[c.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(t){if(e(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var h="alert",u=e.fn[h],d=function(){function t(t){this._element=t}var n=t.prototype;return n.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},n.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},n._getRootElement=function(t){var n=c.getSelectorFromElement(t),i=!1;return n&&(i=document.querySelector(n)),i||(i=e(t).closest(".alert")[0]),i},n._triggerCloseEvent=function(t){var n=e.Event("close.bs.alert");return e(t).trigger(n),n},n._removeElement=function(t){var n=this;if(e(t).removeClass("show"),e(t).hasClass("fade")){var i=c.getTransitionDurationFromElement(t);e(t).one(c.TRANSITION_END,(function(e){return n._destroyElement(t,e)})).emulateTransitionEnd(i)}else this._destroyElement(t)},n._destroyElement=function(t){e(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.alert");o||(o=new t(this),i.data("bs.alert",o)),"close"===n&&o[n](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',d._handleDismiss(new d)),e.fn[h]=d._jQueryInterface,e.fn[h].Constructor=d,e.fn[h].noConflict=function(){return e.fn[h]=u,d._jQueryInterface};var f=e.fn.button,g=function(){function t(t){this._element=t}var n=t.prototype;return n.toggle=function(){var t=!0,n=!0,i=e(this._element).closest('[data-toggle="buttons"]')[0];if(i){var o=this._element.querySelector('input:not([type="hidden"])');if(o){if("radio"===o.type)if(o.checked&&this._element.classList.contains("active"))t=!1;else{var s=i.querySelector(".active");s&&e(s).removeClass("active")}t&&("checkbox"!==o.type&&"radio"!==o.type||(o.checked=!this._element.classList.contains("active")),e(o).trigger("change")),o.focus(),n=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(n&&this._element.setAttribute("aria-pressed",!this._element.classList.contains("active")),t&&e(this._element).toggleClass("active"))},n.dispose=function(){e.removeData(this._element,"bs.button"),this._element=null},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.button");i||(i=new t(this),e(this).data("bs.button",i)),"toggle"===n&&i[n]()}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=t.target,i=n;if(e(n).hasClass("btn")||(n=e(n).closest(".btn")[0]),!n||n.hasAttribute("disabled")||n.classList.contains("disabled"))t.preventDefault();else{var o=n.querySelector('input:not([type="hidden"])');if(o&&(o.hasAttribute("disabled")||o.classList.contains("disabled")))return void t.preventDefault();"LABEL"===i.tagName&&o&&"checkbox"===o.type&&t.preventDefault(),g._jQueryInterface.call(e(n),"toggle")}})).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=e(t.target).closest(".btn")[0];e(n).toggleClass("focus",/^focus(in)?$/.test(t.type))})),e(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e<n;e++){var i=t[e],o=i.querySelector('input:not([type="hidden"])');o.checked||o.hasAttribute("checked")?i.classList.add("active"):i.classList.remove("active")}for(var s=0,r=(t=[].slice.call(document.querySelectorAll('[data-toggle="button"]'))).length;s<r;s++){var a=t[s];"true"===a.getAttribute("aria-pressed")?a.classList.add("active"):a.classList.remove("active")}})),e.fn.button=g._jQueryInterface,e.fn.button.Constructor=g,e.fn.button.noConflict=function(){return e.fn.button=f,g._jQueryInterface};var m="carousel",p=".bs.carousel",_=e.fn[m],v={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},b={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},y={TOUCH:"touch",PEN:"pen"},E=function(){function t(t,e){this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._element=t,this._indicatorsElement=this._element.querySelector(".carousel-indicators"),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var n=t.prototype;return n.next=function(){this._isSliding||this._slide("next")},n.nextWhenVisible=function(){!document.hidden&&e(this._element).is(":visible")&&"hidden"!==e(this._element).css("visibility")&&this.next()},n.prev=function(){this._isSliding||this._slide("prev")},n.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(c.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},n.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},n.to=function(t){var n=this;this._activeElement=this._element.querySelector(".active.carousel-item");var i=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)e(this._element).one("slid.bs.carousel",(function(){return n.to(t)}));else{if(i===t)return this.pause(),void this.cycle();var o=t>i?"next":"prev";this._slide(o,this._items[t])}},n.dispose=function(){e(this._element).off(p),e.removeData(this._element,"bs.carousel"),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},n._getConfig=function(t){return t=a(a({},v),t),c.typeCheckConfig(m,t,b),t},n._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},n._addEventListeners=function(){var t=this;this._config.keyboard&&e(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&e(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},n._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var n=function(e){t._pointerEvent&&y[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},i=function(e){t._pointerEvent&&y[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};e(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(e(this._element).on("pointerdown.bs.carousel",(function(t){return n(t)})),e(this._element).on("pointerup.bs.carousel",(function(t){return i(t)})),this._element.classList.add("pointer-event")):(e(this._element).on("touchstart.bs.carousel",(function(t){return n(t)})),e(this._element).on("touchmove.bs.carousel",(function(e){return function(e){e.originalEvent.touches&&e.originalEvent.touches.length>1?t.touchDeltaX=0:t.touchDeltaX=e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),e(this._element).on("touchend.bs.carousel",(function(t){return i(t)})))}},n._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},n._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},n._getItemByDirection=function(t,e){var n="next"===t,i="prev"===t,o=this._getItemIndex(e),s=this._items.length-1;if((i&&0===o||n&&o===s)&&!this._config.wrap)return e;var r=(o+("prev"===t?-1:1))%this._items.length;return-1===r?this._items[this._items.length-1]:this._items[r]},n._triggerSlideEvent=function(t,n){var i=this._getItemIndex(t),o=this._getItemIndex(this._element.querySelector(".active.carousel-item")),s=e.Event("slide.bs.carousel",{relatedTarget:t,direction:n,from:o,to:i});return e(this._element).trigger(s),s},n._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var n=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));e(n).removeClass("active");var i=this._indicatorsElement.children[this._getItemIndex(t)];i&&e(i).addClass("active")}},n._slide=function(t,n){var i,o,s,r=this,a=this._element.querySelector(".active.carousel-item"),l=this._getItemIndex(a),h=n||a&&this._getItemByDirection(t,a),u=this._getItemIndex(h),d=Boolean(this._interval);if("next"===t?(i="carousel-item-left",o="carousel-item-next",s="left"):(i="carousel-item-right",o="carousel-item-prev",s="right"),h&&e(h).hasClass("active"))this._isSliding=!1;else if(!this._triggerSlideEvent(h,s).isDefaultPrevented()&&a&&h){this._isSliding=!0,d&&this.pause(),this._setActiveIndicatorElement(h);var f=e.Event("slid.bs.carousel",{relatedTarget:h,direction:s,from:l,to:u});if(e(this._element).hasClass("slide")){e(h).addClass(o),c.reflow(h),e(a).addClass(i),e(h).addClass(i);var g=parseInt(h.getAttribute("data-interval"),10);g?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=g):this._config.interval=this._config.defaultInterval||this._config.interval;var m=c.getTransitionDurationFromElement(a);e(a).one(c.TRANSITION_END,(function(){e(h).removeClass(i+" "+o).addClass("active"),e(a).removeClass("active "+o+" "+i),r._isSliding=!1,setTimeout((function(){return e(r._element).trigger(f)}),0)})).emulateTransitionEnd(m)}else e(a).removeClass("active"),e(h).addClass("active"),this._isSliding=!1,e(this._element).trigger(f);d&&this.cycle()}},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.carousel"),o=a(a({},v),e(this).data());"object"==typeof n&&(o=a(a({},o),n));var s="string"==typeof n?n:o.slide;if(i||(i=new t(this,o),e(this).data("bs.carousel",i)),"number"==typeof n)i.to(n);else if("string"==typeof s){if("undefined"==typeof i[s])throw new TypeError('No method named "'+s+'"');i[s]()}else o.interval&&o.ride&&(i.pause(),i.cycle())}))},t._dataApiClickHandler=function(n){var i=c.getSelectorFromElement(this);if(i){var o=e(i)[0];if(o&&e(o).hasClass("carousel")){var s=a(a({},e(o).data()),e(this).data()),r=this.getAttribute("data-slide-to");r&&(s.interval=!1),t._jQueryInterface.call(e(o),s),r&&e(o).data("bs.carousel").to(r),n.preventDefault()}}},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return v}}]),t}();e(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",E._dataApiClickHandler),e(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),n=0,i=t.length;n<i;n++){var o=e(t[n]);E._jQueryInterface.call(o,o.data())}})),e.fn[m]=E._jQueryInterface,e.fn[m].Constructor=E,e.fn[m].noConflict=function(){return e.fn[m]=_,E._jQueryInterface};var w="collapse",T=e.fn[w],C={toggle:!0,parent:""},S={toggle:"boolean",parent:"(string|element)"},D=function(){function t(t,e){this._isTransitioning=!1,this._element=t,this._config=this._getConfig(e),this._triggerArray=[].slice.call(document.querySelectorAll('[data-toggle="collapse"][href="#'+t.id+'"],[data-toggle="collapse"][data-target="#'+t.id+'"]'));for(var n=[].slice.call(document.querySelectorAll('[data-toggle="collapse"]')),i=0,o=n.length;i<o;i++){var s=n[i],r=c.getSelectorFromElement(s),a=[].slice.call(document.querySelectorAll(r)).filter((function(e){return e===t}));null!==r&&a.length>0&&(this._selector=r,this._triggerArray.push(s))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var n=t.prototype;return n.toggle=function(){e(this._element).hasClass("show")?this.hide():this.show()},n.show=function(){var n,i,o=this;if(!this._isTransitioning&&!e(this._element).hasClass("show")&&(this._parent&&0===(n=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof o._config.parent?t.getAttribute("data-parent")===o._config.parent:t.classList.contains("collapse")}))).length&&(n=null),!(n&&(i=e(n).not(this._selector).data("bs.collapse"))&&i._isTransitioning))){var s=e.Event("show.bs.collapse");if(e(this._element).trigger(s),!s.isDefaultPrevented()){n&&(t._jQueryInterface.call(e(n).not(this._selector),"hide"),i||e(n).data("bs.collapse",null));var r=this._getDimension();e(this._element).removeClass("collapse").addClass("collapsing"),this._element.style[r]=0,this._triggerArray.length&&e(this._triggerArray).removeClass("collapsed").attr("aria-expanded",!0),this.setTransitioning(!0);var a="scroll"+(r[0].toUpperCase()+r.slice(1)),l=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,(function(){e(o._element).removeClass("collapsing").addClass("collapse show"),o._element.style[r]="",o.setTransitioning(!1),e(o._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(l),this._element.style[r]=this._element[a]+"px"}}},n.hide=function(){var t=this;if(!this._isTransitioning&&e(this._element).hasClass("show")){var n=e.Event("hide.bs.collapse");if(e(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",c.reflow(this._element),e(this._element).addClass("collapsing").removeClass("collapse show");var o=this._triggerArray.length;if(o>0)for(var s=0;s<o;s++){var r=this._triggerArray[s],a=c.getSelectorFromElement(r);if(null!==a)e([].slice.call(document.querySelectorAll(a))).hasClass("show")||e(r).addClass("collapsed").attr("aria-expanded",!1)}this.setTransitioning(!0);this._element.style[i]="";var l=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,(function(){t.setTransitioning(!1),e(t._element).removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")})).emulateTransitionEnd(l)}}},n.setTransitioning=function(t){this._isTransitioning=t},n.dispose=function(){e.removeData(this._element,"bs.collapse"),this._config=null,this._parent=null,this._element=null,this._triggerArray=null,this._isTransitioning=null},n._getConfig=function(t){return(t=a(a({},C),t)).toggle=Boolean(t.toggle),c.typeCheckConfig(w,t,S),t},n._getDimension=function(){return e(this._element).hasClass("width")?"width":"height"},n._getParent=function(){var n,i=this;c.isElement(this._config.parent)?(n=this._config.parent,"undefined"!=typeof this._config.parent.jquery&&(n=this._config.parent[0])):n=document.querySelector(this._config.parent);var o='[data-toggle="collapse"][data-parent="'+this._config.parent+'"]',s=[].slice.call(n.querySelectorAll(o));return e(s).each((function(e,n){i._addAriaAndCollapsedClass(t._getTargetFromElement(n),[n])})),n},n._addAriaAndCollapsedClass=function(t,n){var i=e(t).hasClass("show");n.length&&e(n).toggleClass("collapsed",!i).attr("aria-expanded",i)},t._getTargetFromElement=function(t){var e=c.getSelectorFromElement(t);return e?document.querySelector(e):null},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.collapse"),s=a(a(a({},C),i.data()),"object"==typeof n&&n?n:{});if(!o&&s.toggle&&"string"==typeof n&&/show|hide/.test(n)&&(s.toggle=!1),o||(o=new t(this,s),i.data("bs.collapse",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return C}}]),t}();e(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',(function(t){"A"===t.currentTarget.tagName&&t.preventDefault();var n=e(this),i=c.getSelectorFromElement(this),o=[].slice.call(document.querySelectorAll(i));e(o).each((function(){var t=e(this),i=t.data("bs.collapse")?"toggle":n.data();D._jQueryInterface.call(t,i)}))})),e.fn[w]=D._jQueryInterface,e.fn[w].Constructor=D,e.fn[w].noConflict=function(){return e.fn[w]=T,D._jQueryInterface};var k="dropdown",N=e.fn[k],A=new RegExp("38|40|27"),I={offset:0,flip:!0,boundary:"scrollParent",reference:"toggle",display:"dynamic",popperConfig:null},O={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)",reference:"(string|element)",display:"string",popperConfig:"(null|object)"},j=function(){function t(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var i=t.prototype;return i.toggle=function(){if(!this._element.disabled&&!e(this._element).hasClass("disabled")){var n=e(this._menu).hasClass("show");t._clearMenus(),n||this.show(!0)}},i.show=function(i){if(void 0===i&&(i=!1),!(this._element.disabled||e(this._element).hasClass("disabled")||e(this._menu).hasClass("show"))){var o={relatedTarget:this._element},s=e.Event("show.bs.dropdown",o),r=t._getParentFromElement(this._element);if(e(r).trigger(s),!s.isDefaultPrevented()){if(!this._inNavbar&&i){if("undefined"==typeof n)throw new TypeError("Bootstrap's dropdowns require Popper.js (https://popper.js.org/)");var a=this._element;"parent"===this._config.reference?a=r:c.isElement(this._config.reference)&&(a=this._config.reference,"undefined"!=typeof this._config.reference.jquery&&(a=this._config.reference[0])),"scrollParent"!==this._config.boundary&&e(r).addClass("position-static"),this._popper=new n(a,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===e(r).closest(".navbar-nav").length&&e(document.body).children().on("mouseover",null,e.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),e(this._menu).toggleClass("show"),e(r).toggleClass("show").trigger(e.Event("shown.bs.dropdown",o))}}},i.hide=function(){if(!this._element.disabled&&!e(this._element).hasClass("disabled")&&e(this._menu).hasClass("show")){var n={relatedTarget:this._element},i=e.Event("hide.bs.dropdown",n),o=t._getParentFromElement(this._element);e(o).trigger(i),i.isDefaultPrevented()||(this._popper&&this._popper.destroy(),e(this._menu).toggleClass("show"),e(o).toggleClass("show").trigger(e.Event("hidden.bs.dropdown",n)))}},i.dispose=function(){e.removeData(this._element,"bs.dropdown"),e(this._element).off(".bs.dropdown"),this._element=null,this._menu=null,null!==this._popper&&(this._popper.destroy(),this._popper=null)},i.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},i._addEventListeners=function(){var t=this;e(this._element).on("click.bs.dropdown",(function(e){e.preventDefault(),e.stopPropagation(),t.toggle()}))},i._getConfig=function(t){return t=a(a(a({},this.constructor.Default),e(this._element).data()),t),c.typeCheckConfig(k,t,this.constructor.DefaultType),t},i._getMenuElement=function(){if(!this._menu){var e=t._getParentFromElement(this._element);e&&(this._menu=e.querySelector(".dropdown-menu"))}return this._menu},i._getPlacement=function(){var t=e(this._element.parentNode),n="bottom-start";return t.hasClass("dropup")?n=e(this._menu).hasClass("dropdown-menu-right")?"top-end":"top-start":t.hasClass("dropright")?n="right-start":t.hasClass("dropleft")?n="left-start":e(this._menu).hasClass("dropdown-menu-right")&&(n="bottom-end"),n},i._detectNavbar=function(){return e(this._element).closest(".navbar").length>0},i._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=a(a({},e.offsets),t._config.offset(e.offsets,t._element)||{}),e}:e.offset=this._config.offset,e},i._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),a(a({},t),this._config.popperConfig)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.dropdown");if(i||(i=new t(this,"object"==typeof n?n:null),e(this).data("bs.dropdown",i)),"string"==typeof n){if("undefined"==typeof i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},t._clearMenus=function(n){if(!n||3!==n.which&&("keyup"!==n.type||9===n.which))for(var i=[].slice.call(document.querySelectorAll('[data-toggle="dropdown"]')),o=0,s=i.length;o<s;o++){var r=t._getParentFromElement(i[o]),a=e(i[o]).data("bs.dropdown"),l={relatedTarget:i[o]};if(n&&"click"===n.type&&(l.clickEvent=n),a){var c=a._menu;if(e(r).hasClass("show")&&!(n&&("click"===n.type&&/input|textarea/i.test(n.target.tagName)||"keyup"===n.type&&9===n.which)&&e.contains(r,n.target))){var h=e.Event("hide.bs.dropdown",l);e(r).trigger(h),h.isDefaultPrevented()||("ontouchstart"in document.documentElement&&e(document.body).children().off("mouseover",null,e.noop),i[o].setAttribute("aria-expanded","false"),a._popper&&a._popper.destroy(),e(c).removeClass("show"),e(r).removeClass("show").trigger(e.Event("hidden.bs.dropdown",l)))}}}},t._getParentFromElement=function(t){var e,n=c.getSelectorFromElement(t);return n&&(e=document.querySelector(n)),e||t.parentNode},t._dataApiKeydownHandler=function(n){if(!(/input|textarea/i.test(n.target.tagName)?32===n.which||27!==n.which&&(40!==n.which&&38!==n.which||e(n.target).closest(".dropdown-menu").length):!A.test(n.which))&&!this.disabled&&!e(this).hasClass("disabled")){var i=t._getParentFromElement(this),o=e(i).hasClass("show");if(o||27!==n.which){if(n.preventDefault(),n.stopPropagation(),!o||o&&(27===n.which||32===n.which))return 27===n.which&&e(i.querySelector('[data-toggle="dropdown"]')).trigger("focus"),void e(this).trigger("click");var s=[].slice.call(i.querySelectorAll(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)")).filter((function(t){return e(t).is(":visible")}));if(0!==s.length){var r=s.indexOf(n.target);38===n.which&&r>0&&r--,40===n.which&&r<s.length-1&&r++,r<0&&(r=0),s[r].focus()}}}},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return I}},{key:"DefaultType",get:function(){return O}}]),t}();e(document).on("keydown.bs.dropdown.data-api",'[data-toggle="dropdown"]',j._dataApiKeydownHandler).on("keydown.bs.dropdown.data-api",".dropdown-menu",j._dataApiKeydownHandler).on("click.bs.dropdown.data-api keyup.bs.dropdown.data-api",j._clearMenus).on("click.bs.dropdown.data-api",'[data-toggle="dropdown"]',(function(t){t.preventDefault(),t.stopPropagation(),j._jQueryInterface.call(e(this),"toggle")})).on("click.bs.dropdown.data-api",".dropdown form",(function(t){t.stopPropagation()})),e.fn[k]=j._jQueryInterface,e.fn[k].Constructor=j,e.fn[k].noConflict=function(){return e.fn[k]=N,j._jQueryInterface};var P=e.fn.modal,x={backdrop:!0,keyboard:!0,focus:!0,show:!0},L={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean",show:"boolean"},R=function(){function t(t,e){this._config=this._getConfig(e),this._element=t,this._dialog=t.querySelector(".modal-dialog"),this._backdrop=null,this._isShown=!1,this._isBodyOverflowing=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollbarWidth=0}var n=t.prototype;return n.toggle=function(t){return this._isShown?this.hide():this.show(t)},n.show=function(t){var n=this;if(!this._isShown&&!this._isTransitioning){e(this._element).hasClass("fade")&&(this._isTransitioning=!0);var i=e.Event("show.bs.modal",{relatedTarget:t});e(this._element).trigger(i),this._isShown||i.isDefaultPrevented()||(this._isShown=!0,this._checkScrollbar(),this._setScrollbar(),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),e(this._element).on("click.dismiss.bs.modal",'[data-dismiss="modal"]',(function(t){return n.hide(t)})),e(this._dialog).on("mousedown.dismiss.bs.modal",(function(){e(n._element).one("mouseup.dismiss.bs.modal",(function(t){e(t.target).is(n._element)&&(n._ignoreBackdropClick=!0)}))})),this._showBackdrop((function(){return n._showElement(t)})))}},n.hide=function(t){var n=this;if(t&&t.preventDefault(),this._isShown&&!this._isTransitioning){var i=e.Event("hide.bs.modal");if(e(this._element).trigger(i),this._isShown&&!i.isDefaultPrevented()){this._isShown=!1;var o=e(this._element).hasClass("fade");if(o&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),e(document).off("focusin.bs.modal"),e(this._element).removeClass("show"),e(this._element).off("click.dismiss.bs.modal"),e(this._dialog).off("mousedown.dismiss.bs.modal"),o){var s=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,(function(t){return n._hideModal(t)})).emulateTransitionEnd(s)}else this._hideModal()}}},n.dispose=function(){[window,this._element,this._dialog].forEach((function(t){return e(t).off(".bs.modal")})),e(document).off("focusin.bs.modal"),e.removeData(this._element,"bs.modal"),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},n.handleUpdate=function(){this._adjustDialog()},n._getConfig=function(t){return t=a(a({},x),t),c.typeCheckConfig("modal",t,L),t},n._triggerBackdropTransition=function(){var t=this;if("static"===this._config.backdrop){var n=e.Event("hidePrevented.bs.modal");if(e(this._element).trigger(n),n.defaultPrevented)return;this._element.classList.add("modal-static");var i=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,(function(){t._element.classList.remove("modal-static")})).emulateTransitionEnd(i),this._element.focus()}else this.hide()},n._showElement=function(t){var n=this,i=e(this._element).hasClass("fade"),o=this._dialog?this._dialog.querySelector(".modal-body"):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),e(this._dialog).hasClass("modal-dialog-scrollable")&&o?o.scrollTop=0:this._element.scrollTop=0,i&&c.reflow(this._element),e(this._element).addClass("show"),this._config.focus&&this._enforceFocus();var s=e.Event("shown.bs.modal",{relatedTarget:t}),r=function(){n._config.focus&&n._element.focus(),n._isTransitioning=!1,e(n._element).trigger(s)};if(i){var a=c.getTransitionDurationFromElement(this._dialog);e(this._dialog).one(c.TRANSITION_END,r).emulateTransitionEnd(a)}else r()},n._enforceFocus=function(){var t=this;e(document).off("focusin.bs.modal").on("focusin.bs.modal",(function(n){document!==n.target&&t._element!==n.target&&0===e(t._element).has(n.target).length&&t._element.focus()}))},n._setEscapeEvent=function(){var t=this;this._isShown?e(this._element).on("keydown.dismiss.bs.modal",(function(e){t._config.keyboard&&27===e.which?(e.preventDefault(),t.hide()):t._config.keyboard||27!==e.which||t._triggerBackdropTransition()})):this._isShown||e(this._element).off("keydown.dismiss.bs.modal")},n._setResizeEvent=function(){var t=this;this._isShown?e(window).on("resize.bs.modal",(function(e){return t.handleUpdate(e)})):e(window).off("resize.bs.modal")},n._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._isTransitioning=!1,this._showBackdrop((function(){e(document.body).removeClass("modal-open"),t._resetAdjustments(),t._resetScrollbar(),e(t._element).trigger("hidden.bs.modal")}))},n._removeBackdrop=function(){this._backdrop&&(e(this._backdrop).remove(),this._backdrop=null)},n._showBackdrop=function(t){var n=this,i=e(this._element).hasClass("fade")?"fade":"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",i&&this._backdrop.classList.add(i),e(this._backdrop).appendTo(document.body),e(this._element).on("click.dismiss.bs.modal",(function(t){n._ignoreBackdropClick?n._ignoreBackdropClick=!1:t.target===t.currentTarget&&n._triggerBackdropTransition()})),i&&c.reflow(this._backdrop),e(this._backdrop).addClass("show"),!t)return;if(!i)return void t();var o=c.getTransitionDurationFromElement(this._backdrop);e(this._backdrop).one(c.TRANSITION_END,t).emulateTransitionEnd(o)}else if(!this._isShown&&this._backdrop){e(this._backdrop).removeClass("show");var s=function(){n._removeBackdrop(),t&&t()};if(e(this._element).hasClass("fade")){var r=c.getTransitionDurationFromElement(this._backdrop);e(this._backdrop).one(c.TRANSITION_END,s).emulateTransitionEnd(r)}else s()}else t&&t()},n._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},n._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},n._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)<window.innerWidth,this._scrollbarWidth=this._getScrollbarWidth()},n._setScrollbar=function(){var t=this;if(this._isBodyOverflowing){var n=[].slice.call(document.querySelectorAll(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top")),i=[].slice.call(document.querySelectorAll(".sticky-top"));e(n).each((function(n,i){var o=i.style.paddingRight,s=e(i).css("padding-right");e(i).data("padding-right",o).css("padding-right",parseFloat(s)+t._scrollbarWidth+"px")})),e(i).each((function(n,i){var o=i.style.marginRight,s=e(i).css("margin-right");e(i).data("margin-right",o).css("margin-right",parseFloat(s)-t._scrollbarWidth+"px")}));var o=document.body.style.paddingRight,s=e(document.body).css("padding-right");e(document.body).data("padding-right",o).css("padding-right",parseFloat(s)+this._scrollbarWidth+"px")}e(document.body).addClass("modal-open")},n._resetScrollbar=function(){var t=[].slice.call(document.querySelectorAll(".fixed-top, .fixed-bottom, .is-fixed, .sticky-top"));e(t).each((function(t,n){var i=e(n).data("padding-right");e(n).removeData("padding-right"),n.style.paddingRight=i||""}));var n=[].slice.call(document.querySelectorAll(".sticky-top"));e(n).each((function(t,n){var i=e(n).data("margin-right");"undefined"!=typeof i&&e(n).css("margin-right",i).removeData("margin-right")}));var i=e(document.body).data("padding-right");e(document.body).removeData("padding-right"),document.body.style.paddingRight=i||""},n._getScrollbarWidth=function(){var t=document.createElement("div");t.className="modal-scrollbar-measure",document.body.appendChild(t);var e=t.getBoundingClientRect().width-t.clientWidth;return document.body.removeChild(t),e},t._jQueryInterface=function(n,i){return this.each((function(){var o=e(this).data("bs.modal"),s=a(a(a({},x),e(this).data()),"object"==typeof n&&n?n:{});if(o||(o=new t(this,s),e(this).data("bs.modal",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n](i)}else s.show&&o.show(i)}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return x}}]),t}();e(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',(function(t){var n,i=this,o=c.getSelectorFromElement(this);o&&(n=document.querySelector(o));var s=e(n).data("bs.modal")?"toggle":a(a({},e(n).data()),e(this).data());"A"!==this.tagName&&"AREA"!==this.tagName||t.preventDefault();var r=e(n).one("show.bs.modal",(function(t){t.isDefaultPrevented()||r.one("hidden.bs.modal",(function(){e(i).is(":visible")&&i.focus()}))}));R._jQueryInterface.call(e(n),s,this)})),e.fn.modal=R._jQueryInterface,e.fn.modal.Constructor=R,e.fn.modal.noConflict=function(){return e.fn.modal=P,R._jQueryInterface};var q=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],F={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Q=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/gi,B=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i;function H(t,e,n){if(0===t.length)return t;if(n&&"function"==typeof n)return n(t);for(var i=(new window.DOMParser).parseFromString(t,"text/html"),o=Object.keys(e),s=[].slice.call(i.body.querySelectorAll("*")),r=function(t,n){var i=s[t],r=i.nodeName.toLowerCase();if(-1===o.indexOf(i.nodeName.toLowerCase()))return i.parentNode.removeChild(i),"continue";var a=[].slice.call(i.attributes),l=[].concat(e["*"]||[],e[r]||[]);a.forEach((function(t){(function(t,e){var n=t.nodeName.toLowerCase();if(-1!==e.indexOf(n))return-1===q.indexOf(n)||Boolean(t.nodeValue.match(Q)||t.nodeValue.match(B));for(var i=e.filter((function(t){return t instanceof RegExp})),o=0,s=i.length;o<s;o++)if(n.match(i[o]))return!0;return!1})(t,l)||i.removeAttribute(t.nodeName)}))},a=0,l=s.length;a<l;a++)r(a);return i.body.innerHTML}var U="tooltip",M=e.fn[U],W=new RegExp("(^|\\s)bs-tooltip\\S+","g"),V=["sanitize","whiteList","sanitizeFn"],z={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",popperConfig:"(null|object)"},K={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"},X={animation:!0,template:'<div class="tooltip" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:F,popperConfig:null},Y={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},$=function(){function t(t,e){if("undefined"==typeof n)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var i=t.prototype;return i.enable=function(){this._isEnabled=!0},i.disable=function(){this._isEnabled=!1},i.toggleEnabled=function(){this._isEnabled=!this._isEnabled},i.toggle=function(t){if(this._isEnabled)if(t){var n=this.constructor.DATA_KEY,i=e(t.currentTarget).data(n);i||(i=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(e(this.getTipElement()).hasClass("show"))return void this._leave(null,this);this._enter(null,this)}},i.dispose=function(){clearTimeout(this._timeout),e.removeData(this.element,this.constructor.DATA_KEY),e(this.element).off(this.constructor.EVENT_KEY),e(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&e(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},i.show=function(){var t=this;if("none"===e(this.element).css("display"))throw new Error("Please use show on visible elements");var i=e.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){e(this.element).trigger(i);var o=c.findShadowRoot(this.element),s=e.contains(null!==o?o:this.element.ownerDocument.documentElement,this.element);if(i.isDefaultPrevented()||!s)return;var r=this.getTipElement(),a=c.getUID(this.constructor.NAME);r.setAttribute("id",a),this.element.setAttribute("aria-describedby",a),this.setContent(),this.config.animation&&e(r).addClass("fade");var l="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,h=this._getAttachment(l);this.addAttachmentClass(h);var u=this._getContainer();e(r).data(this.constructor.DATA_KEY,this),e.contains(this.element.ownerDocument.documentElement,this.tip)||e(r).appendTo(u),e(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new n(this.element,r,this._getPopperConfig(h)),e(r).addClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().on("mouseover",null,e.noop);var d=function(){t.config.animation&&t._fixTransition();var n=t._hoverState;t._hoverState=null,e(t.element).trigger(t.constructor.Event.SHOWN),"out"===n&&t._leave(null,t)};if(e(this.tip).hasClass("fade")){var f=c.getTransitionDurationFromElement(this.tip);e(this.tip).one(c.TRANSITION_END,d).emulateTransitionEnd(f)}else d()}},i.hide=function(t){var n=this,i=this.getTipElement(),o=e.Event(this.constructor.Event.HIDE),s=function(){"show"!==n._hoverState&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),e(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()};if(e(this.element).trigger(o),!o.isDefaultPrevented()){if(e(i).removeClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().off("mouseover",null,e.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,e(this.tip).hasClass("fade")){var r=c.getTransitionDurationFromElement(i);e(i).one(c.TRANSITION_END,s).emulateTransitionEnd(r)}else s();this._hoverState=""}},i.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},i.isWithContent=function(){return Boolean(this.getTitle())},i.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-tooltip-"+t)},i.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},i.setContent=function(){var t=this.getTipElement();this.setElementContent(e(t.querySelectorAll(".tooltip-inner")),this.getTitle()),e(t).removeClass("fade show")},i.setElementContent=function(t,n){"object"!=typeof n||!n.nodeType&&!n.jquery?this.config.html?(this.config.sanitize&&(n=H(n,this.config.whiteList,this.config.sanitizeFn)),t.html(n)):t.text(n):this.config.html?e(n).parent().is(t)||t.empty().append(n):t.text(e(n).text())},i.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},i._getPopperConfig=function(t){var e=this;return a(a({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),this.config.popperConfig)},i._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=a(a({},e.offsets),t.config.offset(e.offsets,t.element)||{}),e}:e.offset=this.config.offset,e},i._getContainer=function(){return!1===this.config.container?document.body:c.isElement(this.config.container)?e(this.config.container):e(document).find(this.config.container)},i._getAttachment=function(t){return K[t.toUpperCase()]},i._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(n){if("click"===n)e(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==n){var i="hover"===n?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,o="hover"===n?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;e(t.element).on(i,t.config.selector,(function(e){return t._enter(e)})).on(o,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},e(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=a(a({},this.config),{},{trigger:"manual",selector:""}):this._fixTitle()},i._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},i._enter=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e(n.getTipElement()).hasClass("show")||"show"===n._hoverState?n._hoverState="show":(clearTimeout(n._timeout),n._hoverState="show",n.config.delay&&n.config.delay.show?n._timeout=setTimeout((function(){"show"===n._hoverState&&n.show()}),n.config.delay.show):n.show())},i._leave=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusout"===t.type?"focus":"hover"]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState="out",n.config.delay&&n.config.delay.hide?n._timeout=setTimeout((function(){"out"===n._hoverState&&n.hide()}),n.config.delay.hide):n.hide())},i._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},i._getConfig=function(t){var n=e(this.element).data();return Object.keys(n).forEach((function(t){-1!==V.indexOf(t)&&delete n[t]})),"number"==typeof(t=a(a(a({},this.constructor.Default),n),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),c.typeCheckConfig(U,t,this.constructor.DefaultType),t.sanitize&&(t.template=H(t.template,t.whiteList,t.sanitizeFn)),t},i._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},i._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(W);null!==n&&n.length&&t.removeClass(n.join(""))},i._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},i._fixTransition=function(){var t=this.getTipElement(),n=this.config.animation;null===t.getAttribute("x-placement")&&(e(t).removeClass("fade"),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.tooltip"),o="object"==typeof n&&n;if((i||!/dispose|hide/.test(n))&&(i||(i=new t(this,o),e(this).data("bs.tooltip",i)),"string"==typeof n)){if("undefined"==typeof i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return X}},{key:"NAME",get:function(){return U}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return Y}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return z}}]),t}();e.fn[U]=$._jQueryInterface,e.fn[U].Constructor=$,e.fn[U].noConflict=function(){return e.fn[U]=M,$._jQueryInterface};var J="popover",G=e.fn[J],Z=new RegExp("(^|\\s)bs-popover\\S+","g"),tt=a(a({},$.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>'}),et=a(a({},$.DefaultType),{},{content:"(string|element|function)"}),nt={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},it=function(t){var n,i;function s(){return t.apply(this,arguments)||this}i=t,(n=s).prototype=Object.create(i.prototype),n.prototype.constructor=n,n.__proto__=i;var r=s.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-popover-"+t)},r.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},r.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(t.find(".popover-body"),n),t.removeClass("fade show")},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(Z);null!==n&&n.length>0&&t.removeClass(n.join(""))},s._jQueryInterface=function(t){return this.each((function(){var n=e(this).data("bs.popover"),i="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new s(this,i),e(this).data("bs.popover",n)),"string"==typeof t)){if("undefined"==typeof n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},o(s,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return tt}},{key:"NAME",get:function(){return J}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return nt}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return et}}]),s}($);e.fn[J]=it._jQueryInterface,e.fn[J].Constructor=it,e.fn[J].noConflict=function(){return e.fn[J]=G,it._jQueryInterface};var ot="scrollspy",st=e.fn[ot],rt={offset:10,method:"auto",target:""},at={offset:"number",method:"string",target:"(string|element)"},lt=function(){function t(t,n){var i=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(n),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,e(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return i._process(t)})),this.refresh(),this._process()}var n=t.prototype;return n.refresh=function(){var t=this,n=this._scrollElement===this._scrollElement.window?"offset":"position",i="auto"===this._config.method?n:this._config.method,o="position"===i?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var n,s=c.getSelectorFromElement(t);if(s&&(n=document.querySelector(s)),n){var r=n.getBoundingClientRect();if(r.width||r.height)return[e(n)[i]().top+o,s]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},n.dispose=function(){e.removeData(this._element,"bs.scrollspy"),e(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},n._getConfig=function(t){if("string"!=typeof(t=a(a({},rt),"object"==typeof t&&t?t:{})).target&&c.isElement(t.target)){var n=e(t.target).attr("id");n||(n=c.getUID(ot),e(t.target).attr("id",n)),t.target="#"+n}return c.typeCheckConfig(ot,t,at),t},n._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},n._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},n._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},n._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t<this._offsets[0]&&this._offsets[0]>0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t<this._offsets[o+1])&&this._activate(this._targets[o])}}},n._activate=function(t){this._activeTarget=t,this._clear();var n=this._selector.split(",").map((function(e){return e+'[data-target="'+t+'"],'+e+'[href="'+t+'"]'})),i=e([].slice.call(document.querySelectorAll(n.join(","))));i.hasClass("dropdown-item")?(i.closest(".dropdown").find(".dropdown-toggle").addClass("active"),i.addClass("active")):(i.addClass("active"),i.parents(".nav, .list-group").prev(".nav-link, .list-group-item").addClass("active"),i.parents(".nav, .list-group").prev(".nav-item").children(".nav-link").addClass("active")),e(this._scrollElement).trigger("activate.bs.scrollspy",{relatedTarget:t})},n._clear=function(){[].slice.call(document.querySelectorAll(this._selector)).filter((function(t){return t.classList.contains("active")})).forEach((function(t){return t.classList.remove("active")}))},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.scrollspy");if(i||(i=new t(this,"object"==typeof n&&n),e(this).data("bs.scrollspy",i)),"string"==typeof n){if("undefined"==typeof i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return rt}}]),t}();e(window).on("load.bs.scrollspy.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-spy="scroll"]')),n=t.length;n--;){var i=e(t[n]);lt._jQueryInterface.call(i,i.data())}})),e.fn[ot]=lt._jQueryInterface,e.fn[ot].Constructor=lt,e.fn[ot].noConflict=function(){return e.fn[ot]=st,lt._jQueryInterface};var ct=e.fn.tab,ht=function(){function t(t){this._element=t}var n=t.prototype;return n.show=function(){var t=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&e(this._element).hasClass("active")||e(this._element).hasClass("disabled"))){var n,i,o=e(this._element).closest(".nav, .list-group")[0],s=c.getSelectorFromElement(this._element);if(o){var r="UL"===o.nodeName||"OL"===o.nodeName?"> li > .active":".active";i=(i=e.makeArray(e(o).find(r)))[i.length-1]}var a=e.Event("hide.bs.tab",{relatedTarget:this._element}),l=e.Event("show.bs.tab",{relatedTarget:i});if(i&&e(i).trigger(a),e(this._element).trigger(l),!l.isDefaultPrevented()&&!a.isDefaultPrevented()){s&&(n=document.querySelector(s)),this._activate(this._element,o);var h=function(){var n=e.Event("hidden.bs.tab",{relatedTarget:t._element}),o=e.Event("shown.bs.tab",{relatedTarget:i});e(i).trigger(n),e(t._element).trigger(o)};n?this._activate(n,n.parentNode,h):h()}}},n.dispose=function(){e.removeData(this._element,"bs.tab"),this._element=null},n._activate=function(t,n,i){var o=this,s=(!n||"UL"!==n.nodeName&&"OL"!==n.nodeName?e(n).children(".active"):e(n).find("> li > .active"))[0],r=i&&s&&e(s).hasClass("fade"),a=function(){return o._transitionComplete(t,s,i)};if(s&&r){var l=c.getTransitionDurationFromElement(s);e(s).removeClass("show").one(c.TRANSITION_END,a).emulateTransitionEnd(l)}else a()},n._transitionComplete=function(t,n,i){if(n){e(n).removeClass("active");var o=e(n.parentNode).find("> .dropdown-menu .active")[0];o&&e(o).removeClass("active"),"tab"===n.getAttribute("role")&&n.setAttribute("aria-selected",!1)}if(e(t).addClass("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),c.reflow(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&e(t.parentNode).hasClass("dropdown-menu")){var s=e(t).closest(".dropdown")[0];if(s){var r=[].slice.call(s.querySelectorAll(".dropdown-toggle"));e(r).addClass("active")}t.setAttribute("aria-expanded",!0)}i&&i()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.tab");if(o||(o=new t(this),i.data("bs.tab",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n]()}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),ht._jQueryInterface.call(e(this),"show")})),e.fn.tab=ht._jQueryInterface,e.fn.tab.Constructor=ht,e.fn.tab.noConflict=function(){return e.fn.tab=ct,ht._jQueryInterface};var ut=e.fn.toast,dt={animation:"boolean",autohide:"boolean",delay:"number"},ft={animation:!0,autohide:!0,delay:500},gt=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var n=t.prototype;return n.show=function(){var t=this,n=e.Event("show.bs.toast");if(e(this._element).trigger(n),!n.isDefaultPrevented()){this._config.animation&&this._element.classList.add("fade");var i=function(){t._element.classList.remove("showing"),t._element.classList.add("show"),e(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove("hide"),c.reflow(this._element),this._element.classList.add("showing"),this._config.animation){var o=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,i).emulateTransitionEnd(o)}else i()}},n.hide=function(){if(this._element.classList.contains("show")){var t=e.Event("hide.bs.toast");e(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},n.dispose=function(){clearTimeout(this._timeout),this._timeout=null,this._element.classList.contains("show")&&this._element.classList.remove("show"),e(this._element).off("click.dismiss.bs.toast"),e.removeData(this._element,"bs.toast"),this._element=null,this._config=null},n._getConfig=function(t){return t=a(a(a({},ft),e(this._element).data()),"object"==typeof t&&t?t:{}),c.typeCheckConfig("toast",t,this.constructor.DefaultType),t},n._setListeners=function(){var t=this;e(this._element).on("click.dismiss.bs.toast",'[data-dismiss="toast"]',(function(){return t.hide()}))},n._close=function(){var t=this,n=function(){t._element.classList.add("hide"),e(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var i=c.getTransitionDurationFromElement(this._element);e(this._element).one(c.TRANSITION_END,n).emulateTransitionEnd(i)}else n()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.toast");if(o||(o=new t(this,"object"==typeof n&&n),i.data("bs.toast",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n](this)}}))},o(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"DefaultType",get:function(){return dt}},{key:"Default",get:function(){return ft}}]),t}();e.fn.toast=gt._jQueryInterface,e.fn.toast.Constructor=gt,e.fn.toast.noConflict=function(){return e.fn.toast=ut,gt._jQueryInterface},t.Alert=d,t.Button=g,t.Carousel=E,t.Collapse=D,t.Dropdown=j,t.Modal=R,t.Popover=it,t.Scrollspy=lt,t.Tab=ht,t.Toast=gt,t.Tooltip=$,t.Util=c,Object.defineProperty(t,"__esModule",{value:!0})}));-//# sourceMappingURL=bootstrap.min.js.map
@@ -1,1 +0,0 @@-{"version":3,"sources":["../../js/src/util.js","../../js/src/alert.js","../../js/src/button.js","../../js/src/carousel.js","../../js/src/collapse.js","../../js/src/dropdown.js","../../js/src/modal.js","../../js/src/tools/sanitizer.js","../../js/src/tooltip.js","../../js/src/popover.js","../../js/src/scrollspy.js","../../js/src/tab.js","../../js/src/toast.js"],"names":["transitionEndEmulator","duration","_this","this","called","$","one","Util","TRANSITION_END","setTimeout","triggerTransitionEnd","getUID","prefix","Math","random","document","getElementById","getSelectorFromElement","element","selector","getAttribute","hrefAttr","trim","querySelector","err","getTransitionDurationFromElement","transitionDuration","css","transitionDelay","floatTransitionDuration","parseFloat","floatTransitionDelay","split","reflow","offsetHeight","trigger","supportsTransitionEnd","Boolean","isElement","obj","nodeType","typeCheckConfig","componentName","config","configTypes","property","Object","prototype","hasOwnProperty","call","expectedTypes","value","valueType","toString","match","toLowerCase","RegExp","test","Error","toUpperCase","findShadowRoot","documentElement","attachShadow","getRootNode","root","ShadowRoot","parentNode","jQueryDetection","TypeError","version","fn","jquery","emulateTransitionEnd","event","special","bindType","delegateType","handle","target","is","handleObj","handler","apply","arguments","NAME","JQUERY_NO_CONFLICT","Alert","_element","close","rootElement","_getRootElement","_triggerCloseEvent","isDefaultPrevented","_removeElement","dispose","removeData","parent","closest","closeEvent","Event","removeClass","hasClass","_destroyElement","detach","remove","_jQueryInterface","each","$element","data","_handleDismiss","alertInstance","preventDefault","on","Constructor","noConflict","Button","toggle","triggerChangeEvent","addAriaPressed","input","type","checked","classList","contains","activeElement","focus","hasAttribute","setAttribute","toggleClass","button","initialButton","inputBtn","tagName","window","buttons","slice","querySelectorAll","i","len","length","add","EVENT_KEY","Default","interval","keyboard","slide","pause","wrap","touch","DefaultType","PointerType","TOUCH","PEN","Carousel","_items","_interval","_activeElement","_isPaused","_isSliding","touchTimeout","touchStartX","touchDeltaX","_config","_getConfig","_indicatorsElement","_touchSupported","navigator","maxTouchPoints","_pointerEvent","PointerEvent","MSPointerEvent","_addEventListeners","next","_slide","nextWhenVisible","hidden","prev","cycle","clearInterval","setInterval","visibilityState","bind","to","index","activeIndex","_getItemIndex","direction","off","_objectSpread2","_handleSwipe","absDeltax","abs","_this2","_keydown","_addTouchEventListeners","_this3","start","originalEvent","pointerType","clientX","touches","end","clearTimeout","e","move","which","indexOf","_getItemByDirection","isNextDirection","isPrevDirection","lastItemIndex","itemIndex","_triggerSlideEvent","relatedTarget","eventDirectionName","targetIndex","fromIndex","slideEvent","from","_setActiveIndicatorElement","indicators","nextIndicator","children","addClass","directionalClassName","orderClassName","_this4","activeElementIndex","nextElement","nextElementIndex","isCycling","slidEvent","nextElementInterval","parseInt","defaultInterval","CLASS_NAME_ACTIVE","action","ride","_dataApiClickHandler","slideIndex","carousels","$carousel","Collapse","_isTransitioning","_triggerArray","id","toggleList","elem","filterElement","filter","foundElem","_selector","push","_parent","_getParent","_addAriaAndCollapsedClass","hide","show","actives","activesData","not","startEvent","dimension","_getDimension","style","attr","setTransitioning","scrollSize","CLASS_NAME_COLLAPSE","getBoundingClientRect","triggerArrayLength","isTransitioning","_getTargetFromElement","triggerArray","isOpen","$this","currentTarget","$trigger","selectors","$target","REGEXP_KEYDOWN","ARROW_UP_KEYCODE","offset","flip","boundary","reference","display","popperConfig","Dropdown","_popper","_menu","_getMenuElement","_inNavbar","_detectNavbar","disabled","isActive","_clearMenus","usePopper","showEvent","_getParentFromElement","Popper","referenceElement","_getPopperConfig","body","noop","hideEvent","destroy","update","scheduleUpdate","stopPropagation","constructor","_getPlacement","$parentDropdown","placement","_getOffset","offsets","modifiers","enabled","preventOverflow","boundariesElement","applyStyle","toggles","context","clickEvent","dropdownMenu","_dataApiKeydownHandler","items","item","EVENT_CLICK_DATA_API","backdrop","Modal","_dialog","_backdrop","_isShown","_isBodyOverflowing","_ignoreBackdropClick","_scrollbarWidth","_checkScrollbar","_setScrollbar","_adjustDialog","_setEscapeEvent","_setResizeEvent","_showBackdrop","_showElement","transition","_hideModal","forEach","htmlElement","handleUpdate","_triggerBackdropTransition","hideEventPrevented","defaultPrevented","modalTransitionDuration","modalBody","Node","ELEMENT_NODE","appendChild","removeAttribute","scrollTop","_enforceFocus","shownEvent","transitionComplete","_this5","has","_this6","_this7","_this8","_resetAdjustments","_resetScrollbar","_removeBackdrop","callback","_this9","animate","createElement","className","appendTo","backdropTransitionDuration","callbackRemove","isModalOverflowing","scrollHeight","clientHeight","paddingLeft","paddingRight","rect","round","left","right","innerWidth","_getScrollbarWidth","_this10","fixedContent","stickyContent","actualPadding","calculatedPadding","actualMargin","marginRight","calculatedMargin","padding","elements","margin","scrollDiv","scrollbarWidth","width","clientWidth","removeChild","_this11","uriAttrs","DefaultWhitelist","*","a","area","b","br","col","code","div","em","hr","h1","h2","h3","h4","h5","h6","img","li","ol","p","pre","s","small","span","sub","sup","strong","u","ul","SAFE_URL_PATTERN","DATA_URL_PATTERN","sanitizeHtml","unsafeHtml","whiteList","sanitizeFn","createdDocument","DOMParser","parseFromString","whitelistKeys","keys","_loop","el","elName","nodeName","attributeList","attributes","whitelistedAttributes","concat","allowedAttributeList","attrName","nodeValue","regExp","attrRegex","allowedAttribute","innerHTML","BSCLS_PREFIX_REGEX","DISALLOWED_ATTRIBUTES","animation","template","title","delay","html","container","fallbackPlacement","sanitize","AttachmentMap","AUTO","TOP","RIGHT","BOTTOM","LEFT","HIDE","HIDDEN","SHOW","SHOWN","INSERTED","CLICK","FOCUSIN","FOCUSOUT","MOUSEENTER","MOUSELEAVE","Tooltip","_isEnabled","_timeout","_hoverState","_activeTrigger","tip","_setListeners","enable","disable","toggleEnabled","dataKey","DATA_KEY","_getDelegateConfig","click","_isWithActiveTrigger","_enter","_leave","getTipElement","_hideModalHandler","isWithContent","shadowRoot","isInTheDom","ownerDocument","tipId","setContent","attachment","_getAttachment","addAttachmentClass","_getContainer","complete","_fixTransition","prevHoverState","_cleanTipClass","getTitle","CLASS_PREFIX","setElementContent","CLASS_NAME_FADE","content","text","empty","append","behavior","arrow","onCreate","originalPlacement","_handlePopperPlacementChange","onUpdate","find","eventIn","eventOut","_fixTitle","titleType","dataAttributes","dataAttr","key","$tip","tabClass","join","popperData","instance","popper","initConfigAnimation","Popover","_getContent","method","ScrollSpy","_scrollElement","_offsets","_targets","_activeTarget","_scrollHeight","_process","refresh","autoMethod","offsetMethod","offsetBase","_getScrollTop","_getScrollHeight","map","targetSelector","targetBCR","height","top","sort","pageYOffset","max","_getOffsetHeight","innerHeight","maxScroll","_activate","_clear","queries","$link","parents","SELECTOR_NAV_LINKS","node","scrollSpys","$spy","Tab","previous","listElement","itemSelector","makeArray","hiddenEvent","active","_transitionComplete","dropdownChild","dropdownElement","dropdownToggleList","autohide","Toast","_close"],"mappings":"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","sourcesContent":["/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): util.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\n\n/**\n * ------------------------------------------------------------------------\n * Private TransitionEnd Helpers\n * ------------------------------------------------------------------------\n */\n\nconst TRANSITION_END = 'transitionend'\nconst MAX_UID = 1000000\nconst MILLISECONDS_MULTIPLIER = 1000\n\n// Shoutout AngusCroll (https://goo.gl/pxwQGp)\nfunction toType(obj) {\n if (obj === null || typeof obj === 'undefined') {\n return `${obj}`\n }\n\n return {}.toString.call(obj).match(/\\s([a-z]+)/i)[1].toLowerCase()\n}\n\nfunction getSpecialTransitionEndEvent() {\n return {\n bindType: TRANSITION_END,\n delegateType: TRANSITION_END,\n handle(event) {\n if ($(event.target).is(this)) {\n return event.handleObj.handler.apply(this, arguments) // eslint-disable-line prefer-rest-params\n }\n return undefined\n }\n }\n}\n\nfunction transitionEndEmulator(duration) {\n let called = false\n\n $(this).one(Util.TRANSITION_END, () => {\n called = true\n })\n\n setTimeout(() => {\n if (!called) {\n Util.triggerTransitionEnd(this)\n }\n }, duration)\n\n return this\n}\n\nfunction setTransitionEndSupport() {\n $.fn.emulateTransitionEnd = transitionEndEmulator\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent()\n}\n\n/**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\nconst Util = {\n TRANSITION_END: 'bsTransitionEnd',\n\n 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 return prefix\n },\n\n getSelectorFromElement(element) {\n let selector = element.getAttribute('data-target')\n\n if (!selector || selector === '#') {\n const 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\n getTransitionDurationFromElement(element) {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let transitionDuration = $(element).css('transition-duration')\n let transitionDelay = $(element).css('transition-delay')\n\n const floatTransitionDuration = parseFloat(transitionDuration)\n const floatTransitionDelay = parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n },\n\n reflow(element) {\n return element.offsetHeight\n },\n\n triggerTransitionEnd(element) {\n $(element).trigger(TRANSITION_END)\n },\n\n // TODO: Remove in v5\n supportsTransitionEnd() {\n return Boolean(TRANSITION_END)\n },\n\n isElement(obj) {\n return (obj[0] || obj).nodeType\n },\n\n typeCheckConfig(componentName, config, configTypes) {\n for (const property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && Util.isElement(value)\n ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`)\n }\n }\n }\n },\n\n findShadowRoot(element) {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return Util.findShadowRoot(element.parentNode)\n },\n\n jQueryDetection() {\n if (typeof $ === 'undefined') {\n throw new TypeError('Bootstrap\\'s JavaScript requires jQuery. jQuery must be included before Bootstrap\\'s JavaScript.')\n }\n\n const version = $.fn.jquery.split(' ')[0].split('.')\n const minMajor = 1\n const ltMajor = 2\n const minMinor = 9\n const minPatch = 1\n const 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\nUtil.jQueryDetection()\nsetTransitionEndSupport()\n\nexport default Util\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'alert'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst SELECTOR_DISMISS = '[data-dismiss=\"alert\"]'\n\nconst EVENT_CLOSE = `close${EVENT_KEY}`\nconst EVENT_CLOSED = `closed${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_ALERT = 'alert'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Alert {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n close(element) {\n let rootElement = this._element\n if (element) {\n rootElement = this._getRootElement(element)\n }\n\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent.isDefaultPrevented()) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _getRootElement(element) {\n const selector = Util.getSelectorFromElement(element)\n let parent = false\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n if (!parent) {\n parent = $(element).closest(`.${CLASS_NAME_ALERT}`)[0]\n }\n\n return parent\n }\n\n _triggerCloseEvent(element) {\n const closeEvent = $.Event(EVENT_CLOSE)\n\n $(element).trigger(closeEvent)\n return closeEvent\n }\n\n _removeElement(element) {\n $(element).removeClass(CLASS_NAME_SHOW)\n\n if (!$(element).hasClass(CLASS_NAME_FADE)) {\n this._destroyElement(element)\n return\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(element)\n\n $(element)\n .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event))\n .emulateTransitionEnd(transitionDuration)\n }\n\n _destroyElement(element) {\n $(element)\n .detach()\n .trigger(EVENT_CLOSED)\n .remove()\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let 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 static _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(\n EVENT_CLICK_DATA_API,\n SELECTOR_DISMISS,\n Alert._handleDismiss(new Alert())\n)\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Alert._jQueryInterface\n$.fn[NAME].Constructor = Alert\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Alert._jQueryInterface\n}\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'button'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_BUTTON = 'btn'\nconst CLASS_NAME_FOCUS = 'focus'\n\nconst SELECTOR_DATA_TOGGLE_CARROT = '[data-toggle^=\"button\"]'\nconst SELECTOR_DATA_TOGGLES = '[data-toggle=\"buttons\"]'\nconst SELECTOR_DATA_TOGGLE = '[data-toggle=\"button\"]'\nconst SELECTOR_DATA_TOGGLES_BUTTONS = '[data-toggle=\"buttons\"] .btn'\nconst SELECTOR_INPUT = 'input:not([type=\"hidden\"])'\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_BUTTON = '.btn'\n\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_FOCUS_BLUR_DATA_API = `focus${EVENT_KEY}${DATA_API_KEY} ` +\n `blur${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Button {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n toggle() {\n let triggerChangeEvent = true\n let addAriaPressed = true\n const rootElement = $(this._element).closest(\n SELECTOR_DATA_TOGGLES\n )[0]\n\n if (rootElement) {\n const input = this._element.querySelector(SELECTOR_INPUT)\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked &&\n this._element.classList.contains(CLASS_NAME_ACTIVE)) {\n triggerChangeEvent = false\n } else {\n const activeElement = rootElement.querySelector(SELECTOR_ACTIVE)\n\n if (activeElement) {\n $(activeElement).removeClass(CLASS_NAME_ACTIVE)\n }\n }\n }\n\n if (triggerChangeEvent) {\n // if it's not a radio button or checkbox don't add a pointless/invalid checked property to the input\n if (input.type === 'checkbox' || input.type === 'radio') {\n input.checked = !this._element.classList.contains(CLASS_NAME_ACTIVE)\n }\n $(input).trigger('change')\n }\n\n input.focus()\n addAriaPressed = false\n }\n }\n\n if (!(this._element.hasAttribute('disabled') || this._element.classList.contains('disabled'))) {\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed',\n !this._element.classList.contains(CLASS_NAME_ACTIVE))\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(CLASS_NAME_ACTIVE)\n }\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE_CARROT, (event) => {\n let button = event.target\n const initialButton = button\n\n if (!$(button).hasClass(CLASS_NAME_BUTTON)) {\n button = $(button).closest(SELECTOR_BUTTON)[0]\n }\n\n if (!button || button.hasAttribute('disabled') || button.classList.contains('disabled')) {\n event.preventDefault() // work around Firefox bug #1540995\n } else {\n const inputBtn = button.querySelector(SELECTOR_INPUT)\n\n if (inputBtn && (inputBtn.hasAttribute('disabled') || inputBtn.classList.contains('disabled'))) {\n event.preventDefault() // work around Firefox bug #1540995\n return\n }\n\n if (initialButton.tagName === 'LABEL' && inputBtn && inputBtn.type === 'checkbox') {\n event.preventDefault() // work around event sent to label and input\n }\n Button._jQueryInterface.call($(button), 'toggle')\n }\n })\n .on(EVENT_FOCUS_BLUR_DATA_API, SELECTOR_DATA_TOGGLE_CARROT, (event) => {\n const button = $(event.target).closest(SELECTOR_BUTTON)[0]\n $(button).toggleClass(CLASS_NAME_FOCUS, /^focus(in)?$/.test(event.type))\n })\n\n$(window).on(EVENT_LOAD_DATA_API, () => {\n // ensure correct active class is set to match the controls' actual values/states\n\n // find all checkboxes/readio buttons inside data-toggle groups\n let buttons = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLES_BUTTONS))\n for (let i = 0, len = buttons.length; i < len; i++) {\n const button = buttons[i]\n const input = button.querySelector(SELECTOR_INPUT)\n if (input.checked || input.hasAttribute('checked')) {\n button.classList.add(CLASS_NAME_ACTIVE)\n } else {\n button.classList.remove(CLASS_NAME_ACTIVE)\n }\n }\n\n // find all button toggles\n buttons = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE))\n for (let i = 0, len = buttons.length; i < len; i++) {\n const button = buttons[i]\n if (button.getAttribute('aria-pressed') === 'true') {\n button.classList.add(CLASS_NAME_ACTIVE)\n } else {\n button.classList.remove(CLASS_NAME_ACTIVE)\n }\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Button._jQueryInterface\n$.fn[NAME].Constructor = Button\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Button._jQueryInterface\n}\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'carousel'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key\nconst ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n interval : 5000,\n keyboard : true,\n slide : false,\n pause : 'hover',\n wrap : true,\n touch : true\n}\n\nconst DefaultType = {\n interval : '(number|boolean)',\n keyboard : 'boolean',\n slide : '(boolean|string)',\n pause : '(string|boolean)',\n wrap : 'boolean',\n touch : 'boolean'\n}\n\nconst DIRECTION_NEXT = 'next'\nconst DIRECTION_PREV = 'prev'\nconst DIRECTION_LEFT = 'left'\nconst DIRECTION_RIGHT = 'right'\n\nconst EVENT_SLIDE = `slide${EVENT_KEY}`\nconst EVENT_SLID = `slid${EVENT_KEY}`\nconst EVENT_KEYDOWN = `keydown${EVENT_KEY}`\nconst EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`\nconst EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY}`\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY}`\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_CAROUSEL = 'carousel'\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_SLIDE = 'slide'\nconst CLASS_NAME_RIGHT = 'carousel-item-right'\nconst CLASS_NAME_LEFT = 'carousel-item-left'\nconst CLASS_NAME_NEXT = 'carousel-item-next'\nconst CLASS_NAME_PREV = 'carousel-item-prev'\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event'\n\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_ACTIVE_ITEM = '.active.carousel-item'\nconst SELECTOR_ITEM = '.carousel-item'\nconst SELECTOR_ITEM_IMG = '.carousel-item img'\nconst SELECTOR_NEXT_PREV = '.carousel-item-next, .carousel-item-prev'\nconst SELECTOR_INDICATORS = '.carousel-indicators'\nconst SELECTOR_DATA_SLIDE = '[data-slide], [data-slide-to]'\nconst SELECTOR_DATA_RIDE = '[data-ride=\"carousel\"]'\n\nconst PointerType = {\n TOUCH : 'touch',\n PEN : 'pen'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\nclass Carousel {\n constructor(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\n this._config = this._getConfig(config)\n this._element = element\n this._indicatorsElement = this._element.querySelector(SELECTOR_INDICATORS)\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(DIRECTION_NEXT)\n }\n }\n\n 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 &&\n ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(DIRECTION_PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if (this._element.querySelector(SELECTOR_NEXT_PREV)) {\n Util.triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n 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(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = this._element.querySelector(SELECTOR_ACTIVE_ITEM)\n\n const 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_SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const direction = index > activeIndex\n ? DIRECTION_NEXT\n : DIRECTION_PREV\n\n this._slide(direction, this._items[index])\n }\n\n dispose() {\n $(this._element).off(EVENT_KEY)\n $.removeData(this._element, DATA_KEY)\n\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 }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _handleSwipe() {\n const absDeltax = Math.abs(this.touchDeltaX)\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltax / this.touchDeltaX\n\n this.touchDeltaX = 0\n\n // swipe left\n if (direction > 0) {\n this.prev()\n }\n\n // swipe right\n if (direction < 0) {\n this.next()\n }\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n $(this._element).on(EVENT_KEYDOWN, (event) => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n $(this._element)\n .on(EVENT_MOUSEENTER, (event) => this.pause(event))\n .on(EVENT_MOUSELEAVE, (event) => this.cycle(event))\n }\n\n if (this._config.touch) {\n this._addTouchEventListeners()\n }\n }\n\n _addTouchEventListeners() {\n if (!this._touchSupported) {\n return\n }\n\n const start = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchStartX = event.originalEvent.clientX\n } else if (!this._pointerEvent) {\n this.touchStartX = event.originalEvent.touches[0].clientX\n }\n }\n\n const move = (event) => {\n // ensure swiping with one touch and not pinching\n if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n this.touchDeltaX = 0\n } else {\n this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX\n }\n }\n\n const end = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchDeltaX = event.originalEvent.clientX - this.touchStartX\n }\n\n this._handleSwipe()\n if (this._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\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n }\n\n $(this._element.querySelectorAll(SELECTOR_ITEM_IMG))\n .on(EVENT_DRAG_START, (e) => e.preventDefault())\n\n if (this._pointerEvent) {\n $(this._element).on(EVENT_POINTERDOWN, (event) => start(event))\n $(this._element).on(EVENT_POINTERUP, (event) => end(event))\n\n this._element.classList.add(CLASS_NAME_POINTER_EVENT)\n } else {\n $(this._element).on(EVENT_TOUCHSTART, (event) => start(event))\n $(this._element).on(EVENT_TOUCHMOVE, (event) => move(event))\n $(this._element).on(EVENT_TOUCHEND, (event) => end(event))\n }\n }\n\n _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 case ARROW_RIGHT_KEYCODE:\n event.preventDefault()\n this.next()\n break\n default:\n }\n }\n\n _getItemIndex(element) {\n this._items = element && element.parentNode\n ? [].slice.call(element.parentNode.querySelectorAll(SELECTOR_ITEM))\n : []\n return this._items.indexOf(element)\n }\n\n _getItemByDirection(direction, activeElement) {\n const isNextDirection = direction === DIRECTION_NEXT\n const isPrevDirection = direction === DIRECTION_PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = isPrevDirection && activeIndex === 0 ||\n isNextDirection && activeIndex === lastItemIndex\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = direction === DIRECTION_PREV ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1\n ? this._items[this._items.length - 1] : this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex(this._element.querySelector(SELECTOR_ACTIVE_ITEM))\n const slideEvent = $.Event(EVENT_SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n\n $(this._element).trigger(slideEvent)\n\n return slideEvent\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(SELECTOR_ACTIVE))\n $(indicators).removeClass(CLASS_NAME_ACTIVE)\n\n const nextIndicator = this._indicatorsElement.children[\n this._getItemIndex(element)\n ]\n\n if (nextIndicator) {\n $(nextIndicator).addClass(CLASS_NAME_ACTIVE)\n }\n }\n }\n\n _slide(direction, element) {\n const activeElement = this._element.querySelector(SELECTOR_ACTIVE_ITEM)\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || activeElement &&\n this._getItemByDirection(direction, activeElement)\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n let directionalClassName\n let orderClassName\n let eventDirectionName\n\n if (direction === DIRECTION_NEXT) {\n directionalClassName = CLASS_NAME_LEFT\n orderClassName = CLASS_NAME_NEXT\n eventDirectionName = DIRECTION_LEFT\n } else {\n directionalClassName = CLASS_NAME_RIGHT\n orderClassName = CLASS_NAME_PREV\n eventDirectionName = DIRECTION_RIGHT\n }\n\n if (nextElement && $(nextElement).hasClass(CLASS_NAME_ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\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 const slidEvent = $.Event(EVENT_SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n\n if ($(this._element).hasClass(CLASS_NAME_SLIDE)) {\n $(nextElement).addClass(orderClassName)\n\n Util.reflow(nextElement)\n\n $(activeElement).addClass(directionalClassName)\n $(nextElement).addClass(directionalClassName)\n\n const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)\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 const transitionDuration = Util.getTransitionDurationFromElement(activeElement)\n\n $(activeElement)\n .one(Util.TRANSITION_END, () => {\n $(nextElement)\n .removeClass(`${directionalClassName} ${orderClassName}`)\n .addClass(CLASS_NAME_ACTIVE)\n\n $(activeElement).removeClass(`${CLASS_NAME_ACTIVE} ${orderClassName} ${directionalClassName}`)\n\n this._isSliding = false\n\n setTimeout(() => $(this._element).trigger(slidEvent), 0)\n })\n .emulateTransitionEnd(transitionDuration)\n } else {\n $(activeElement).removeClass(CLASS_NAME_ACTIVE)\n $(nextElement).addClass(CLASS_NAME_ACTIVE)\n\n this._isSliding = false\n $(this._element).trigger(slidEvent)\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n let _config = {\n ...Default,\n ...$(this).data()\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(this, _config)\n $(this).data(DATA_KEY, 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 data[action]()\n } else if (_config.interval && _config.ride) {\n data.pause()\n data.cycle()\n }\n })\n }\n\n static _dataApiClickHandler(event) {\n const selector = Util.getSelectorFromElement(this)\n\n if (!selector) {\n return\n }\n\n const target = $(selector)[0]\n\n if (!target || !$(target).hasClass(CLASS_NAME_CAROUSEL)) {\n return\n }\n\n const config = {\n ...$(target).data(),\n ...$(this).data()\n }\n const 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).to(slideIndex)\n }\n\n event.preventDefault()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, Carousel._dataApiClickHandler)\n\n$(window).on(EVENT_LOAD_DATA_API, () => {\n const carousels = [].slice.call(document.querySelectorAll(SELECTOR_DATA_RIDE))\n for (let i = 0, len = carousels.length; i < len; i++) {\n const $carousel = $(carousels[i])\n Carousel._jQueryInterface.call($carousel, $carousel.data())\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Carousel._jQueryInterface\n$.fn[NAME].Constructor = Carousel\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Carousel._jQueryInterface\n}\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'collapse'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n toggle : true,\n parent : ''\n}\n\nconst DefaultType = {\n toggle : 'boolean',\n parent : '(string|element)'\n}\n\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_COLLAPSE = 'collapse'\nconst CLASS_NAME_COLLAPSING = 'collapsing'\nconst CLASS_NAME_COLLAPSED = 'collapsed'\n\nconst DIMENSION_WIDTH = 'width'\nconst DIMENSION_HEIGHT = 'height'\n\nconst SELECTOR_ACTIVES = '.show, .collapsing'\nconst SELECTOR_DATA_TOGGLE = '[data-toggle=\"collapse\"]'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Collapse {\n constructor(element, config) {\n this._isTransitioning = false\n this._element = element\n this._config = this._getConfig(config)\n this._triggerArray = [].slice.call(document.querySelectorAll(\n `[data-toggle=\"collapse\"][href=\"#${element.id}\"],` +\n `[data-toggle=\"collapse\"][data-target=\"#${element.id}\"]`\n ))\n\n const toggleList = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE))\n for (let i = 0, len = toggleList.length; i < len; i++) {\n const elem = toggleList[i]\n const selector = Util.getSelectorFromElement(elem)\n const filterElement = [].slice.call(document.querySelectorAll(selector))\n .filter((foundElem) => foundElem === element)\n\n if (selector !== null && filterElement.length > 0) {\n this._selector = selector\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 }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle() {\n if ($(this._element).hasClass(CLASS_NAME_SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning ||\n $(this._element).hasClass(CLASS_NAME_SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = [].slice.call(this._parent.querySelectorAll(SELECTOR_ACTIVES))\n .filter((elem) => {\n if (typeof this._config.parent === 'string') {\n return elem.getAttribute('data-parent') === this._config.parent\n }\n\n return elem.classList.contains(CLASS_NAME_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)\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = $.Event(EVENT_SHOW)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')\n if (!activesData) {\n $(actives).data(DATA_KEY, null)\n }\n }\n\n const dimension = this._getDimension()\n\n $(this._element)\n .removeClass(CLASS_NAME_COLLAPSE)\n .addClass(CLASS_NAME_COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length) {\n $(this._triggerArray)\n .removeClass(CLASS_NAME_COLLAPSED)\n .attr('aria-expanded', true)\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n $(this._element)\n .removeClass(CLASS_NAME_COLLAPSING)\n .addClass(`${CLASS_NAME_COLLAPSE} ${CLASS_NAME_SHOW}`)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n $(this._element).trigger(EVENT_SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning ||\n !$(this._element).hasClass(CLASS_NAME_SHOW)) {\n return\n }\n\n const startEvent = $.Event(EVENT_HIDE)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n Util.reflow(this._element)\n\n $(this._element)\n .addClass(CLASS_NAME_COLLAPSING)\n .removeClass(`${CLASS_NAME_COLLAPSE} ${CLASS_NAME_SHOW}`)\n\n const triggerArrayLength = this._triggerArray.length\n if (triggerArrayLength > 0) {\n for (let i = 0; i < triggerArrayLength; i++) {\n const trigger = this._triggerArray[i]\n const selector = Util.getSelectorFromElement(trigger)\n\n if (selector !== null) {\n const $elem = $([].slice.call(document.querySelectorAll(selector)))\n if (!$elem.hasClass(CLASS_NAME_SHOW)) {\n $(trigger).addClass(CLASS_NAME_COLLAPSED)\n .attr('aria-expanded', false)\n }\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n $(this._element)\n .removeClass(CLASS_NAME_COLLAPSING)\n .addClass(CLASS_NAME_COLLAPSE)\n .trigger(EVENT_HIDDEN)\n }\n\n this._element.style[dimension] = ''\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._parent = null\n this._element = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n const hasWidth = $(this._element).hasClass(DIMENSION_WIDTH)\n return hasWidth ? DIMENSION_WIDTH : DIMENSION_HEIGHT\n }\n\n _getParent() {\n let parent\n\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent\n\n // It's a jQuery object\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 const selector = `[data-toggle=\"collapse\"][data-parent=\"${this._config.parent}\"]`\n const children = [].slice.call(parent.querySelectorAll(selector))\n\n $(children).each((i, element) => {\n this._addAriaAndCollapsedClass(\n Collapse._getTargetFromElement(element),\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n const isOpen = $(element).hasClass(CLASS_NAME_SHOW)\n\n if (triggerArray.length) {\n $(triggerArray)\n .toggleClass(CLASS_NAME_COLLAPSED, !isOpen)\n .attr('aria-expanded', isOpen)\n }\n }\n\n // Static\n\n static _getTargetFromElement(element) {\n const selector = Util.getSelectorFromElement(element)\n return selector ? document.querySelector(selector) : null\n }\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n const _config = {\n ...Default,\n ...$this.data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data && _config.toggle && typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(this, _config)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(EVENT_CLICK_DATA_API, SELECTOR_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 const $trigger = $(this)\n const selector = Util.getSelectorFromElement(this)\n const selectors = [].slice.call(document.querySelectorAll(selector))\n\n $(selectors).each(function () {\n const $target = $(this)\n const data = $target.data(DATA_KEY)\n const config = data ? 'toggle' : $trigger.data()\n Collapse._jQueryInterface.call($target, config)\n })\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Collapse._jQueryInterface\n$.fn[NAME].Constructor = Collapse\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Collapse._jQueryInterface\n}\n\nexport default Collapse\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'dropdown'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\nconst SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key\nconst TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key\nconst ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key\nconst ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key\nconst RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)\nconst REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_CLICK = `click${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DISABLED = 'disabled'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_DROPUP = 'dropup'\nconst CLASS_NAME_DROPRIGHT = 'dropright'\nconst CLASS_NAME_DROPLEFT = 'dropleft'\nconst CLASS_NAME_MENURIGHT = 'dropdown-menu-right'\nconst CLASS_NAME_POSITION_STATIC = 'position-static'\n\nconst SELECTOR_DATA_TOGGLE = '[data-toggle=\"dropdown\"]'\nconst SELECTOR_FORM_CHILD = '.dropdown form'\nconst SELECTOR_MENU = '.dropdown-menu'\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav'\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n\nconst PLACEMENT_TOP = 'top-start'\nconst PLACEMENT_TOPEND = 'top-end'\nconst PLACEMENT_BOTTOM = 'bottom-start'\nconst PLACEMENT_BOTTOMEND = 'bottom-end'\nconst PLACEMENT_RIGHT = 'right-start'\nconst PLACEMENT_LEFT = 'left-start'\n\nconst Default = {\n offset : 0,\n flip : true,\n boundary : 'scrollParent',\n reference : 'toggle',\n display : 'dynamic',\n popperConfig : null\n}\n\nconst DefaultType = {\n offset : '(number|string|function)',\n flip : 'boolean',\n boundary : '(string|element)',\n reference : '(string|element)',\n display : 'string',\n popperConfig : '(null|object)'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Dropdown {\n constructor(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 }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED)) {\n return\n }\n\n const isActive = $(this._menu).hasClass(CLASS_NAME_SHOW)\n\n Dropdown._clearMenus()\n\n if (isActive) {\n return\n }\n\n this.show(true)\n }\n\n show(usePopper = false) {\n if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || $(this._menu).hasClass(CLASS_NAME_SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(EVENT_SHOW, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n // Disable totally Popper.js for Dropdown in Navbar\n if (!this._inNavbar && usePopper) {\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 let 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\n\n // Check if it's jQuery element\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0]\n }\n }\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 if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(CLASS_NAME_POSITION_STATIC)\n }\n this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())\n }\n\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 if ('ontouchstart' in document.documentElement &&\n $(parent).closest(SELECTOR_NAVBAR_NAV).length === 0) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n $(this._menu).toggleClass(CLASS_NAME_SHOW)\n $(parent)\n .toggleClass(CLASS_NAME_SHOW)\n .trigger($.Event(EVENT_SHOWN, relatedTarget))\n }\n\n hide() {\n if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || !$(this._menu).hasClass(CLASS_NAME_SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const hideEvent = $.Event(EVENT_HIDE, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n if (this._popper) {\n this._popper.destroy()\n }\n\n $(this._menu).toggleClass(CLASS_NAME_SHOW)\n $(parent)\n .toggleClass(CLASS_NAME_SHOW)\n .trigger($.Event(EVENT_HIDDEN, relatedTarget))\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._element).off(EVENT_KEY)\n this._element = null\n this._menu = null\n if (this._popper !== null) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Private\n\n _addEventListeners() {\n $(this._element).on(EVENT_CLICK, (event) => {\n event.preventDefault()\n event.stopPropagation()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this._element).data(),\n ...config\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getMenuElement() {\n if (!this._menu) {\n const parent = Dropdown._getParentFromElement(this._element)\n\n if (parent) {\n this._menu = parent.querySelector(SELECTOR_MENU)\n }\n }\n return this._menu\n }\n\n _getPlacement() {\n const $parentDropdown = $(this._element.parentNode)\n let placement = PLACEMENT_BOTTOM\n\n // Handle dropup\n if ($parentDropdown.hasClass(CLASS_NAME_DROPUP)) {\n placement = $(this._menu).hasClass(CLASS_NAME_MENURIGHT)\n ? PLACEMENT_TOPEND\n : PLACEMENT_TOP\n } else if ($parentDropdown.hasClass(CLASS_NAME_DROPRIGHT)) {\n placement = PLACEMENT_RIGHT\n } else if ($parentDropdown.hasClass(CLASS_NAME_DROPLEFT)) {\n placement = PLACEMENT_LEFT\n } else if ($(this._menu).hasClass(CLASS_NAME_MENURIGHT)) {\n placement = PLACEMENT_BOTTOMEND\n }\n return placement\n }\n\n _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0\n }\n\n _getOffset() {\n const offset = {}\n\n if (typeof this._config.offset === 'function') {\n offset.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this._config.offset(data.offsets, this._element) || {}\n }\n\n return data\n }\n } else {\n offset.offset = this._config.offset\n }\n\n return offset\n }\n\n _getPopperConfig() {\n const 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 }\n }\n\n // Disable Popper.js if we have a static display\n if (this._config.display === 'static') {\n popperConfig.modifiers.applyStyle = {\n enabled: false\n }\n }\n\n return {\n ...popperConfig,\n ...this._config.popperConfig\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n\n static _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||\n event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return\n }\n\n const toggles = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE))\n\n for (let i = 0, len = toggles.length; i < len; i++) {\n const parent = Dropdown._getParentFromElement(toggles[i])\n const context = $(toggles[i]).data(DATA_KEY)\n const 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 const dropdownMenu = context._menu\n if (!$(parent).hasClass(CLASS_NAME_SHOW)) {\n continue\n }\n\n if (event && (event.type === 'click' &&\n /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&\n $.contains(parent, event.target)) {\n continue\n }\n\n const hideEvent = $.Event(EVENT_HIDE, relatedTarget)\n $(parent).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n if (context._popper) {\n context._popper.destroy()\n }\n\n $(dropdownMenu).removeClass(CLASS_NAME_SHOW)\n $(parent)\n .removeClass(CLASS_NAME_SHOW)\n .trigger($.Event(EVENT_HIDDEN, relatedTarget))\n }\n }\n\n static _getParentFromElement(element) {\n let parent\n const selector = Util.getSelectorFromElement(element)\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n return parent || element.parentNode\n }\n\n // eslint-disable-next-line complexity\n static _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)\n ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&\n (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||\n $(event.target).closest(SELECTOR_MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return\n }\n\n if (this.disabled || $(this).hasClass(CLASS_NAME_DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this)\n const isActive = $(parent).hasClass(CLASS_NAME_SHOW)\n\n if (!isActive && event.which === ESCAPE_KEYCODE) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n $(parent.querySelector(SELECTOR_DATA_TOGGLE)).trigger('focus')\n }\n\n $(this).trigger('click')\n return\n }\n\n const items = [].slice.call(parent.querySelectorAll(SELECTOR_VISIBLE_ITEMS))\n .filter((item) => $(item).is(':visible'))\n\n if (items.length === 0) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up\n index--\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down\n index++\n }\n\n if (index < 0) {\n index = 0\n }\n\n items[index].focus()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown._dataApiKeydownHandler)\n .on(EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown._dataApiKeydownHandler)\n .on(`${EVENT_CLICK_DATA_API} ${EVENT_KEYUP_DATA_API}`, Dropdown._clearMenus)\n .on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n event.preventDefault()\n event.stopPropagation()\n Dropdown._jQueryInterface.call($(this), 'toggle')\n })\n .on(EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, (e) => {\n e.stopPropagation()\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Dropdown._jQueryInterface\n$.fn[NAME].Constructor = Dropdown\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n}\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'modal'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n\nconst Default = {\n backdrop : true,\n keyboard : true,\n focus : true,\n show : true\n}\n\nconst DefaultType = {\n backdrop : '(boolean|string)',\n keyboard : 'boolean',\n focus : 'boolean',\n show : 'boolean'\n}\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_FOCUSIN = `focusin${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEUP_DISMISS = `mouseup.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_SCROLLABLE = 'modal-dialog-scrollable'\nconst CLASS_NAME_SCROLLBAR_MEASURER = 'modal-scrollbar-measure'\nconst CLASS_NAME_BACKDROP = 'modal-backdrop'\nconst CLASS_NAME_OPEN = 'modal-open'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_STATIC = 'modal-static'\n\nconst SELECTOR_DIALOG = '.modal-dialog'\nconst SELECTOR_MODAL_BODY = '.modal-body'\nconst SELECTOR_DATA_TOGGLE = '[data-toggle=\"modal\"]'\nconst SELECTOR_DATA_DISMISS = '[data-dismiss=\"modal\"]'\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top'\nconst SELECTOR_STICKY_CONTENT = '.sticky-top'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Modal {\n constructor(element, config) {\n this._config = this._getConfig(config)\n this._element = element\n this._dialog = element.querySelector(SELECTOR_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 }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n if ($(this._element).hasClass(CLASS_NAME_FADE)) {\n this._isTransitioning = true\n }\n\n const showEvent = $.Event(EVENT_SHOW, {\n relatedTarget\n })\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 this._setScrollbar()\n\n this._adjustDialog()\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(this._element).on(\n EVENT_CLICK_DISMISS,\n SELECTOR_DATA_DISMISS,\n (event) => this.hide(event)\n )\n\n $(this._dialog).on(EVENT_MOUSEDOWN_DISMISS, () => {\n $(this._element).one(EVENT_MOUSEUP_DISMISS, (event) => {\n if ($(event.target).is(this._element)) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = $.Event(EVENT_HIDE)\n\n $(this._element).trigger(hideEvent)\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = false\n const transition = $(this._element).hasClass(CLASS_NAME_FADE)\n\n if (transition) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(document).off(EVENT_FOCUSIN)\n\n $(this._element).removeClass(CLASS_NAME_SHOW)\n\n $(this._element).off(EVENT_CLICK_DISMISS)\n $(this._dialog).off(EVENT_MOUSEDOWN_DISMISS)\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, (event) => this._hideModal(event))\n .emulateTransitionEnd(transitionDuration)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n [window, this._element, this._dialog]\n .forEach((htmlElement) => $(htmlElement).off(EVENT_KEY))\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 $(document).off(EVENT_FOCUSIN)\n\n $.removeData(this._element, DATA_KEY)\n\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 handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _triggerBackdropTransition() {\n if (this._config.backdrop === 'static') {\n const hideEventPrevented = $.Event(EVENT_HIDE_PREVENTED)\n\n $(this._element).trigger(hideEventPrevented)\n if (hideEventPrevented.defaultPrevented) {\n return\n }\n\n this._element.classList.add(CLASS_NAME_STATIC)\n\n const modalTransitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element).one(Util.TRANSITION_END, () => {\n this._element.classList.remove(CLASS_NAME_STATIC)\n })\n .emulateTransitionEnd(modalTransitionDuration)\n this._element.focus()\n } else {\n this.hide()\n }\n }\n\n _showElement(relatedTarget) {\n const transition = $(this._element).hasClass(CLASS_NAME_FADE)\n const modalBody = this._dialog ? this._dialog.querySelector(SELECTOR_MODAL_BODY) : null\n\n if (!this._element.parentNode ||\n 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 this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n\n if ($(this._dialog).hasClass(CLASS_NAME_SCROLLABLE) && modalBody) {\n modalBody.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(CLASS_NAME_SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const shownEvent = $.Event(EVENT_SHOWN, {\n relatedTarget\n })\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n this._isTransitioning = false\n $(this._element).trigger(shownEvent)\n }\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)\n\n $(this._dialog)\n .one(Util.TRANSITION_END, transitionComplete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n $(document)\n .off(EVENT_FOCUSIN) // Guard against infinite focus loop\n .on(EVENT_FOCUSIN, (event) => {\n if (document !== event.target &&\n this._element !== event.target &&\n $(this._element).has(event.target).length === 0) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown) {\n $(this._element).on(EVENT_KEYDOWN_DISMISS, (event) => {\n if (this._config.keyboard && event.which === ESCAPE_KEYCODE) {\n event.preventDefault()\n this.hide()\n } else if (!this._config.keyboard && event.which === ESCAPE_KEYCODE) {\n this._triggerBackdropTransition()\n }\n })\n } else if (!this._isShown) {\n $(this._element).off(EVENT_KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n $(window).on(EVENT_RESIZE, (event) => this.handleUpdate(event))\n } else {\n $(window).off(EVENT_RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._isTransitioning = false\n this._showBackdrop(() => {\n $(document.body).removeClass(CLASS_NAME_OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n $(this._element).trigger(EVENT_HIDDEN)\n })\n }\n\n _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove()\n this._backdrop = null\n }\n }\n\n _showBackdrop(callback) {\n const animate = $(this._element).hasClass(CLASS_NAME_FADE)\n ? CLASS_NAME_FADE : ''\n\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div')\n this._backdrop.className = CLASS_NAME_BACKDROP\n\n if (animate) {\n this._backdrop.classList.add(animate)\n }\n\n $(this._backdrop).appendTo(document.body)\n\n $(this._element).on(EVENT_CLICK_DISMISS, (event) => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n if (event.target !== event.currentTarget) {\n return\n }\n\n this._triggerBackdropTransition()\n })\n\n if (animate) {\n Util.reflow(this._backdrop)\n }\n\n $(this._backdrop).addClass(CLASS_NAME_SHOW)\n\n if (!callback) {\n return\n }\n\n if (!animate) {\n callback()\n return\n }\n\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callback)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(CLASS_NAME_SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n if (callback) {\n callback()\n }\n }\n\n if ($(this._element).hasClass(CLASS_NAME_FADE)) {\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callbackRemove)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else {\n callbackRemove()\n }\n } else if (callback) {\n callback()\n }\n }\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 _adjustDialog() {\n const isModalOverflowing =\n 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 _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = Math.round(rect.left + rect.right) < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\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 const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT))\n const stickyContent = [].slice.call(document.querySelectorAll(SELECTOR_STICKY_CONTENT))\n\n // Adjust fixed content padding\n $(fixedContent).each((index, element) => {\n const actualPadding = element.style.paddingRight\n const calculatedPadding = $(element).css('padding-right')\n $(element)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n })\n\n // Adjust sticky content margin\n $(stickyContent).each((index, element) => {\n const actualMargin = element.style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element)\n .data('margin-right', actualMargin)\n .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)\n })\n\n // Adjust body padding\n const actualPadding = document.body.style.paddingRight\n const calculatedPadding = $(document.body).css('padding-right')\n $(document.body)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n }\n\n $(document.body).addClass(CLASS_NAME_OPEN)\n }\n\n _resetScrollbar() {\n // Restore fixed content padding\n const fixedContent = [].slice.call(document.querySelectorAll(SELECTOR_FIXED_CONTENT))\n $(fixedContent).each((index, element) => {\n const padding = $(element).data('padding-right')\n $(element).removeData('padding-right')\n element.style.paddingRight = padding ? padding : ''\n })\n\n // Restore sticky content\n const elements = [].slice.call(document.querySelectorAll(`${SELECTOR_STICKY_CONTENT}`))\n $(elements).each((index, element) => {\n const margin = $(element).data('margin-right')\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right')\n }\n })\n\n // Restore body padding\n const padding = $(document.body).data('padding-right')\n $(document.body).removeData('padding-right')\n document.body.style.paddingRight = padding ? padding : ''\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = CLASS_NAME_SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = {\n ...Default,\n ...$(this).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data) {\n data = new Modal(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config](relatedTarget)\n } else if (_config.show) {\n data.show(relatedTarget)\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n let target\n const selector = Util.getSelectorFromElement(this)\n\n if (selector) {\n target = document.querySelector(selector)\n }\n\n const config = $(target).data(DATA_KEY)\n ? 'toggle' : {\n ...$(target).data(),\n ...$(this).data()\n }\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n const $target = $(target).one(EVENT_SHOW, (showEvent) => {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return\n }\n\n $target.one(EVENT_HIDDEN, () => {\n if ($(this).is(':visible')) {\n this.focus()\n }\n })\n })\n\n Modal._jQueryInterface.call($(target), config, this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Modal._jQueryInterface\n$.fn[NAME].Constructor = Modal\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Modal._jQueryInterface\n}\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): tools/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst uriAttrs = [\n 'background',\n 'cite',\n 'href',\n 'itemtype',\n 'longdesc',\n 'poster',\n 'src',\n 'xlink:href'\n]\n\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i\n\nexport const 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', 'srcset', '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\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 */\nconst SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/gi\n\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 */\nconst DATA_URL_PATTERN = /^data:(?:image\\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\\/(?:mpeg|mp4|ogg|webm)|audio\\/(?:mp3|oga|ogg|opus));base64,[\\d+/a-z]+=*$/i\n\nfunction allowedAttribute(attr, allowedAttributeList) {\n const 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 const regExp = allowedAttributeList.filter((attrRegex) => attrRegex instanceof RegExp)\n\n // Check if a regular expression validates the attribute.\n for (let i = 0, len = regExp.length; i < len; i++) {\n if (attrName.match(regExp[i])) {\n return true\n }\n }\n\n return false\n}\n\nexport 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 const domParser = new window.DOMParser()\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')\n const whitelistKeys = Object.keys(whiteList)\n const elements = [].slice.call(createdDocument.body.querySelectorAll('*'))\n\n for (let i = 0, len = elements.length; i < len; i++) {\n const el = elements[i]\n const elName = el.nodeName.toLowerCase()\n\n if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {\n el.parentNode.removeChild(el)\n\n continue\n }\n\n const attributeList = [].slice.call(el.attributes)\n const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])\n\n attributeList.forEach((attr) => {\n if (!allowedAttribute(attr, whitelistedAttributes)) {\n el.removeAttribute(attr.nodeName)\n }\n })\n }\n\n return createdDocument.body.innerHTML\n}\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport {\n DefaultWhitelist,\n sanitizeHtml\n} from './tools/sanitizer'\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tooltip'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.tooltip'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-tooltip'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\nconst DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']\n\nconst DefaultType = {\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 popperConfig : '(null|object)'\n}\n\nconst AttachmentMap = {\n AUTO : 'auto',\n TOP : 'top',\n RIGHT : 'right',\n BOTTOM : 'bottom',\n LEFT : 'left'\n}\n\nconst Default = {\n animation : true,\n template : '<div class=\"tooltip\" role=\"tooltip\">' +\n '<div class=\"arrow\"></div>' +\n '<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 popperConfig : null\n}\n\nconst HOVER_STATE_SHOW = 'show'\nconst HOVER_STATE_OUT = 'out'\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner'\nconst SELECTOR_ARROW = '.arrow'\n\nconst TRIGGER_HOVER = 'hover'\nconst TRIGGER_FOCUS = 'focus'\nconst TRIGGER_CLICK = 'click'\nconst TRIGGER_MANUAL = 'manual'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tooltip {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)')\n }\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.element = element\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const dataKey = this.constructor.DATA_KEY\n let context = $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\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(CLASS_NAME_SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n $.removeData(this.element, this.constructor.DATA_KEY)\n\n $(this.element).off(this.constructor.EVENT_KEY)\n $(this.element).closest('.modal').off('hide.bs.modal', this._hideModalHandler)\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 if (this._popper) {\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 show() {\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n const showEvent = $.Event(this.constructor.Event.SHOW)\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent)\n\n const shadowRoot = Util.findShadowRoot(this.element)\n const isInTheDom = $.contains(\n shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,\n this.element\n )\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = Util.getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this.element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n $(tip).addClass(CLASS_NAME_FADE)\n }\n\n const placement = typeof this.config.placement === 'function'\n ? this.config.placement.call(this, tip, this.element)\n : this.config.placement\n\n const attachment = this._getAttachment(placement)\n this.addAttachmentClass(attachment)\n\n const container = this._getContainer()\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\n this._popper = new Popper(this.element, tip, this._getPopperConfig(attachment))\n\n $(tip).addClass(CLASS_NAME_SHOW)\n\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 if ('ontouchstart' in document.documentElement) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n const complete = () => {\n if (this.config.animation) {\n this._fixTransition()\n }\n const prevHoverState = this._hoverState\n this._hoverState = null\n\n $(this.element).trigger(this.constructor.Event.SHOWN)\n\n if (prevHoverState === HOVER_STATE_OUT) {\n this._leave(null, this)\n }\n }\n\n if ($(this.tip).hasClass(CLASS_NAME_FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(this.tip)\n\n $(this.tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n }\n\n hide(callback) {\n const tip = this.getTipElement()\n const hideEvent = $.Event(this.constructor.Event.HIDE)\n const complete = () => {\n if (this._hoverState !== HOVER_STATE_SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this.element.removeAttribute('aria-describedby')\n $(this.element).trigger(this.constructor.Event.HIDDEN)\n if (this._popper !== null) {\n this._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(CLASS_NAME_SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\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(CLASS_NAME_FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(tip)\n\n $(tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const tip = this.getTipElement()\n this.setElementContent($(tip.querySelectorAll(SELECTOR_TOOLTIP_INNER)), this.getTitle())\n $(tip).removeClass(`${CLASS_NAME_FADE} ${CLASS_NAME_SHOW}`)\n }\n\n 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 getTitle() {\n let title = this.element.getAttribute('data-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function'\n ? this.config.title.call(this.element)\n : this.config.title\n }\n\n return title\n }\n\n // Private\n\n _getPopperConfig(attachment) {\n const defaultBsConfig = {\n placement: attachment,\n modifiers: {\n offset: this._getOffset(),\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: SELECTOR_ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: (data) => {\n if (data.originalPlacement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n },\n onUpdate: (data) => this._handlePopperPlacementChange(data)\n }\n\n return {\n ...defaultBsConfig,\n ...this.config.popperConfig\n }\n }\n\n _getOffset() {\n const offset = {}\n\n if (typeof this.config.offset === 'function') {\n offset.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this.config.offset(data.offsets, this.element) || {}\n }\n\n return data\n }\n } else {\n offset.offset = this.config.offset\n }\n\n return offset\n }\n\n _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 _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach((trigger) => {\n if (trigger === 'click') {\n $(this.element).on(\n this.constructor.Event.CLICK,\n this.config.selector,\n (event) => this.toggle(event)\n )\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER\n ? this.constructor.Event.MOUSEENTER\n : this.constructor.Event.FOCUSIN\n const eventOut = trigger === TRIGGER_HOVER\n ? this.constructor.Event.MOUSELEAVE\n : this.constructor.Event.FOCUSOUT\n\n $(this.element)\n .on(eventIn, this.config.selector, (event) => this._enter(event))\n .on(eventOut, this.config.selector, (event) => this._leave(event))\n }\n })\n\n this._hideModalHandler = () => {\n if (this.element) {\n this.hide()\n }\n }\n\n $(this.element).closest('.modal').on('hide.bs.modal', this._hideModalHandler)\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const titleType = typeof this.element.getAttribute('data-original-title')\n\n if (this.element.getAttribute('title') || titleType !== 'string') {\n this.element.setAttribute(\n 'data-original-title',\n this.element.getAttribute('title') || ''\n )\n\n this.element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER\n ] = true\n }\n\n if ($(context.getTipElement()).hasClass(CLASS_NAME_SHOW) || context._hoverState === HOVER_STATE_SHOW) {\n context._hoverState = HOVER_STATE_SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HOVER_STATE_SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HOVER_STATE_SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER\n ] = false\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HOVER_STATE_OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HOVER_STATE_OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n const dataAttributes = $(this.element).data()\n\n Object.keys(dataAttributes)\n .forEach((dataAttr) => {\n if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {\n delete dataAttributes[dataAttr]\n }\n })\n\n config = {\n ...this.constructor.Default,\n ...dataAttributes,\n ...typeof config === 'object' && config ? config : {}\n }\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(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n if (config.sanitize) {\n config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)\n }\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const 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 _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n _handlePopperPlacementChange(popperData) {\n this.tip = popperData.instance.popper\n this._cleanTipClass()\n this.addAttachmentClass(this._getAttachment(popperData.placement))\n }\n\n _fixTransition() {\n const tip = this.getTipElement()\n const initConfigAnimation = this.config.animation\n\n if (tip.getAttribute('x-placement') !== null) {\n return\n }\n\n $(tip).removeClass(CLASS_NAME_FADE)\n this.config.animation = false\n this.hide()\n this.show()\n this.config.animation = initConfigAnimation\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _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, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Tooltip._jQueryInterface\n$.fn[NAME].Constructor = Tooltip\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tooltip._jQueryInterface\n}\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Tooltip from './tooltip'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'popover'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.popover'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-popover'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst Default = {\n ...Tooltip.Default,\n placement : 'right',\n trigger : 'click',\n content : '',\n template : '<div class=\"popover\" role=\"tooltip\">' +\n '<div class=\"arrow\"></div>' +\n '<h3 class=\"popover-header\"></h3>' +\n '<div class=\"popover-body\"></div></div>'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content : '(string|element|function)'\n}\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_TITLE = '.popover-header'\nconst SELECTOR_CONTENT = '.popover-body'\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Popover extends Tooltip {\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n\n // We use append for html objects to maintain js events\n this.setElementContent($tip.find(SELECTOR_TITLE), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this.element)\n }\n this.setElementContent($tip.find(SELECTOR_CONTENT), content)\n\n $tip.removeClass(`${CLASS_NAME_FADE} ${CLASS_NAME_SHOW}`)\n }\n\n // Private\n\n _getContent() {\n return this.element.getAttribute('data-content') ||\n this.config.content\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _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, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Popover._jQueryInterface\n$.fn[NAME].Constructor = Popover\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Popover._jQueryInterface\n}\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'scrollspy'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n offset : 10,\n method : 'auto',\n target : ''\n}\n\nconst DefaultType = {\n offset : 'number',\n method : 'string',\n target : '(string|element)'\n}\n\nconst EVENT_ACTIVATE = `activate${EVENT_KEY}`\nconst EVENT_SCROLL = `scroll${EVENT_KEY}`\nconst EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'\nconst CLASS_NAME_ACTIVE = 'active'\n\nconst SELECTOR_DATA_SPY = '[data-spy=\"scroll\"]'\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'\nconst SELECTOR_NAV_LINKS = '.nav-link'\nconst SELECTOR_NAV_ITEMS = '.nav-item'\nconst SELECTOR_LIST_ITEMS = '.list-group-item'\nconst SELECTOR_DROPDOWN = '.dropdown'\nconst SELECTOR_DROPDOWN_ITEMS = '.dropdown-item'\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'\n\nconst METHOD_OFFSET = 'offset'\nconst METHOD_POSITION = 'position'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass ScrollSpy {\n constructor(element, config) {\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_NAV_LINKS},` +\n `${this._config.target} ${SELECTOR_LIST_ITEMS},` +\n `${this._config.target} ${SELECTOR_DROPDOWN_ITEMS}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n $(this._scrollElement).on(EVENT_SCROLL, (event) => this._process(event))\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window\n ? METHOD_OFFSET : METHOD_POSITION\n\n const offsetMethod = this._config.method === 'auto'\n ? autoMethod : this._config.method\n\n const offsetBase = offsetMethod === METHOD_POSITION\n ? this._getScrollTop() : 0\n\n this._offsets = []\n this._targets = []\n\n this._scrollHeight = this._getScrollHeight()\n\n const targets = [].slice.call(document.querySelectorAll(this._selector))\n\n targets\n .map((element) => {\n let target\n const targetSelector = Util.getSelectorFromElement(element)\n\n if (targetSelector) {\n target = document.querySelector(targetSelector)\n }\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [\n $(target)[offsetMethod]().top + offsetBase,\n targetSelector\n ]\n }\n }\n return null\n })\n .filter((item) => item)\n .sort((a, b) => a[0] - b[0])\n .forEach((item) => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._scrollElement).off(EVENT_KEY)\n\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 }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.target !== 'string' && Util.isElement(config.target)) {\n let id = $(config.target).attr('id')\n if (!id) {\n id = Util.getUID(NAME)\n $(config.target).attr('id', id)\n }\n config.target = `#${id}`\n }\n\n Util.typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window\n ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window\n ? window.innerHeight : this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n for (let i = this._offsets.length; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' ||\n scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n const queries = this._selector\n .split(',')\n .map((selector) => `${selector}[data-target=\"${target}\"],${selector}[href=\"${target}\"]`)\n\n const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))\n\n if ($link.hasClass(CLASS_NAME_DROPDOWN_ITEM)) {\n $link.closest(SELECTOR_DROPDOWN)\n .find(SELECTOR_DROPDOWN_TOGGLE)\n .addClass(CLASS_NAME_ACTIVE)\n $link.addClass(CLASS_NAME_ACTIVE)\n } else {\n // Set triggered link as active\n $link.addClass(CLASS_NAME_ACTIVE)\n // Set triggered links parents as active\n // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor\n $link.parents(SELECTOR_NAV_LIST_GROUP)\n .prev(`${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)\n .addClass(CLASS_NAME_ACTIVE)\n // Handle special case when .nav-link is inside .nav-item\n $link.parents(SELECTOR_NAV_LIST_GROUP)\n .prev(SELECTOR_NAV_ITEMS)\n .children(SELECTOR_NAV_LINKS)\n .addClass(CLASS_NAME_ACTIVE)\n }\n\n $(this._scrollElement).trigger(EVENT_ACTIVATE, {\n relatedTarget: target\n })\n }\n\n _clear() {\n [].slice.call(document.querySelectorAll(this._selector))\n .filter((node) => node.classList.contains(CLASS_NAME_ACTIVE))\n .forEach((node) => node.classList.remove(CLASS_NAME_ACTIVE))\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data) {\n data = new ScrollSpy(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(window).on(EVENT_LOAD_DATA_API, () => {\n const scrollSpys = [].slice.call(document.querySelectorAll(SELECTOR_DATA_SPY))\n const scrollSpysLength = scrollSpys.length\n\n for (let i = scrollSpysLength; i--;) {\n const $spy = $(scrollSpys[i])\n ScrollSpy._jQueryInterface.call($spy, $spy.data())\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = ScrollSpy._jQueryInterface\n$.fn[NAME].Constructor = ScrollSpy\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return ScrollSpy._jQueryInterface\n}\n\nexport default ScrollSpy\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): tab.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tab'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.tab'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_DROPDOWN_MENU = 'dropdown-menu'\nconst CLASS_NAME_ACTIVE = 'active'\nconst CLASS_NAME_DISABLED = 'disabled'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\n\nconst SELECTOR_DROPDOWN = '.dropdown'\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'\nconst SELECTOR_ACTIVE = '.active'\nconst SELECTOR_ACTIVE_UL = '> li > .active'\nconst SELECTOR_DATA_TOGGLE = '[data-toggle=\"tab\"], [data-toggle=\"pill\"], [data-toggle=\"list\"]'\nconst SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'\nconst SELECTOR_DROPDOWN_ACTIVE_CHILD = '> .dropdown-menu .active'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tab {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n show() {\n if (this._element.parentNode &&\n this._element.parentNode.nodeType === Node.ELEMENT_NODE &&\n $(this._element).hasClass(CLASS_NAME_ACTIVE) ||\n $(this._element).hasClass(CLASS_NAME_DISABLED)) {\n return\n }\n\n let target\n let previous\n const listElement = $(this._element).closest(SELECTOR_NAV_LIST_GROUP)[0]\n const selector = Util.getSelectorFromElement(this._element)\n\n if (listElement) {\n const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? SELECTOR_ACTIVE_UL : SELECTOR_ACTIVE\n previous = $.makeArray($(listElement).find(itemSelector))\n previous = previous[previous.length - 1]\n }\n\n const hideEvent = $.Event(EVENT_HIDE, {\n relatedTarget: this._element\n })\n\n const showEvent = $.Event(EVENT_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() ||\n hideEvent.isDefaultPrevented()) {\n return\n }\n\n if (selector) {\n target = document.querySelector(selector)\n }\n\n this._activate(\n this._element,\n listElement\n )\n\n const complete = () => {\n const hiddenEvent = $.Event(EVENT_HIDDEN, {\n relatedTarget: this._element\n })\n\n const shownEvent = $.Event(EVENT_SHOWN, {\n relatedTarget: previous\n })\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 dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _activate(element, container, callback) {\n const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL')\n ? $(container).find(SELECTOR_ACTIVE_UL)\n : $(container).children(SELECTOR_ACTIVE)\n\n const active = activeElements[0]\n const isTransitioning = callback && (active && $(active).hasClass(CLASS_NAME_FADE))\n const complete = () => this._transitionComplete(\n element,\n active,\n callback\n )\n\n if (active && isTransitioning) {\n const transitionDuration = Util.getTransitionDurationFromElement(active)\n\n $(active)\n .removeClass(CLASS_NAME_SHOW)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n\n _transitionComplete(element, active, callback) {\n if (active) {\n $(active).removeClass(CLASS_NAME_ACTIVE)\n\n const dropdownChild = $(active.parentNode).find(\n SELECTOR_DROPDOWN_ACTIVE_CHILD\n )[0]\n\n if (dropdownChild) {\n $(dropdownChild).removeClass(CLASS_NAME_ACTIVE)\n }\n\n if (active.getAttribute('role') === 'tab') {\n active.setAttribute('aria-selected', false)\n }\n }\n\n $(element).addClass(CLASS_NAME_ACTIVE)\n if (element.getAttribute('role') === 'tab') {\n element.setAttribute('aria-selected', true)\n }\n\n Util.reflow(element)\n\n if (element.classList.contains(CLASS_NAME_FADE)) {\n element.classList.add(CLASS_NAME_SHOW)\n }\n\n if (element.parentNode && $(element.parentNode).hasClass(CLASS_NAME_DROPDOWN_MENU)) {\n const dropdownElement = $(element).closest(SELECTOR_DROPDOWN)[0]\n\n if (dropdownElement) {\n const dropdownToggleList = [].slice.call(dropdownElement.querySelectorAll(SELECTOR_DROPDOWN_TOGGLE))\n\n $(dropdownToggleList).addClass(CLASS_NAME_ACTIVE)\n }\n\n element.setAttribute('aria-expanded', true)\n }\n\n if (callback) {\n callback()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n\n if (!data) {\n data = new Tab(this)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n event.preventDefault()\n Tab._jQueryInterface.call($(this), 'show')\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Tab._jQueryInterface\n$.fn[NAME].Constructor = Tab\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tab._jQueryInterface\n}\n\nexport default Tab\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.5.0): toast.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'toast'\nconst VERSION = '4.5.0'\nconst DATA_KEY = 'bs.toast'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\n\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_HIDE = 'hide'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_SHOWING = 'showing'\n\nconst DefaultType = {\n animation : 'boolean',\n autohide : 'boolean',\n delay : 'number'\n}\n\nconst Default = {\n animation : true,\n autohide : true,\n delay : 500\n}\n\nconst SELECTOR_DATA_DISMISS = '[data-dismiss=\"toast\"]'\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Toast {\n constructor(element, config) {\n this._element = element\n this._config = this._getConfig(config)\n this._timeout = null\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n show() {\n const showEvent = $.Event(EVENT_SHOW)\n\n $(this._element).trigger(showEvent)\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n if (this._config.animation) {\n this._element.classList.add(CLASS_NAME_FADE)\n }\n\n const complete = () => {\n this._element.classList.remove(CLASS_NAME_SHOWING)\n this._element.classList.add(CLASS_NAME_SHOW)\n\n $(this._element).trigger(EVENT_SHOWN)\n\n if (this._config.autohide) {\n this._timeout = setTimeout(() => {\n this.hide()\n }, this._config.delay)\n }\n }\n\n this._element.classList.remove(CLASS_NAME_HIDE)\n Util.reflow(this._element)\n this._element.classList.add(CLASS_NAME_SHOWING)\n if (this._config.animation) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n\n hide() {\n if (!this._element.classList.contains(CLASS_NAME_SHOW)) {\n return\n }\n\n const hideEvent = $.Event(EVENT_HIDE)\n\n $(this._element).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._close()\n }\n\n dispose() {\n clearTimeout(this._timeout)\n this._timeout = null\n\n if (this._element.classList.contains(CLASS_NAME_SHOW)) {\n this._element.classList.remove(CLASS_NAME_SHOW)\n }\n\n $(this._element).off(EVENT_CLICK_DISMISS)\n\n $.removeData(this._element, DATA_KEY)\n this._element = null\n this._config = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...$(this._element).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _setListeners() {\n $(this._element).on(EVENT_CLICK_DISMISS, SELECTOR_DATA_DISMISS, () => this.hide())\n }\n\n _close() {\n const complete = () => {\n this._element.classList.add(CLASS_NAME_HIDE)\n $(this._element).trigger(EVENT_HIDDEN)\n }\n\n this._element.classList.remove(CLASS_NAME_SHOW)\n if (this._config.animation) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let data = $element.data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data) {\n data = new Toast(this, _config)\n $element.data(DATA_KEY, 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\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Toast._jQueryInterface\n$.fn[NAME].Constructor = Toast\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Toast._jQueryInterface\n}\n\nexport default Toast\n"]}
@@ -1,50 +1,246 @@ var debugMode = document.currentScript.dataset.debugMode || false; +// Connection states+var ConnectionState = {+ CONNECTING: 'connecting',+ CONNECTED: 'connected',+ DISCONNECTED: 'disconnected',+ RECONNECTING: 'reconnecting',+ FAILED: 'failed'+};++// Configuration+var SSCConfig = {+ maxReconnectAttempts: 10,+ initialReconnectDelay: 1000, // 1 second+ maxReconnectDelay: 30000, // 30 seconds+ reconnectBackoffMultiplier: 1.5,+ maxActionQueueSize: 100 // Maximum queued actions while disconnected+};+ window.callServerAction = function (action, payload) { var component = findActiveComponent(event.target || document.activeElement);- var message = JSON.stringify({ action, ...payload });- component.connection.send(message)+ var message = JSON.stringify({ action: action, ...(payload || {}) });++ // Check connection exists and is connected+ if (component.connection && component.connectionState === ConnectionState.CONNECTED) {+ component.connection.send(message);+ } else {+ // Queue action for replay when reconnected+ component.actionQueue = component.actionQueue || [];+ // Limit queue size to prevent memory issues+ if (component.actionQueue.length < SSCConfig.maxActionQueueSize) {+ component.actionQueue.push(message);+ if (debugMode) console.log('Action queued (not connected):', action);+ } else {+ if (debugMode) console.warn('Action queue full, dropping action:', action);+ }+ } } if (window.Turbolinks) {- document.addEventListener('turbolinks:load', initalizeAllSSCComponents);+ document.addEventListener('turbolinks:load', initializeAllSSCComponents); } else {- initalizeAllSSCComponents();+ initializeAllSSCComponents(); } -function initalizeAllSSCComponents() {+// Reconnect when tab becomes visible+document.addEventListener('visibilitychange', function() {+ if (document.visibilityState === 'visible') {+ for (var component of document.querySelectorAll('.ihp-ssc')) {+ if (component.connectionState === ConnectionState.DISCONNECTED ||+ component.connectionState === ConnectionState.FAILED) {+ if (debugMode) console.log('Tab visible, attempting reconnect');+ reconnectSSC(component);+ }+ }+ }+});++function initializeAllSSCComponents() { for (var component of document.querySelectorAll('.ihp-ssc')) { if (debugMode) console.log('init', component)- initalizeSSC(component);+ initializeSSC(component); } } -function initalizeSSC(component) {+function initializeSSC(component) {+ component.connectionState = ConnectionState.CONNECTING;+ component.reconnectAttempts = 0;+ component.actionQueue = [];++ connectSSC(component);+}++function connectSSC(component) { var socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws'; var socketHost = socketProtocol + "://" + window.location.hostname + ":" + document.location.port; - component.connection = new WebSocket(socketHost + component.dataset.path);+ updateConnectionUI(component, ConnectionState.CONNECTING); + try {+ component.connection = new WebSocket(socketHost + component.dataset.path);+ } catch (e) {+ if (debugMode) console.error('WebSocket creation failed:', e);+ handleConnectionFailure(component);+ return;+ }+ component.connection.onopen = function (event) { if (debugMode) console.log('Connected');+ component.connectionState = ConnectionState.CONNECTED;+ component.reconnectAttempts = 0;+ updateConnectionUI(component, ConnectionState.CONNECTED);++ // Replay queued actions+ if (component.actionQueue && component.actionQueue.length > 0) {+ if (debugMode) console.log('Replaying', component.actionQueue.length, 'queued actions');+ for (var message of component.actionQueue) {+ component.connection.send(message);+ }+ component.actionQueue = [];+ } }; component.connection.onclose = function (event) {- component.innerText = 'This component crashed :(';+ if (debugMode) console.log('Connection closed:', event.code, event.reason);+ component.connectionState = ConnectionState.DISCONNECTED;+ scheduleReconnect(component); }; + component.connection.onerror = function (event) {+ if (debugMode) console.error('WebSocket error:', event);+ // onclose will be called after this, so we don't need to handle reconnection here+ };+ component.connection.onmessage = function (event) { var payload = JSON.parse(event.data);++ // Check if this is an error message from the server+ if (payload.type && payload.type.endsWith('Error')) {+ handleServerError(component, payload);+ return;+ }+ evaluatePatch(component, payload); if (debugMode) console.log(payload);- //var newElement = document.createElement('div');- //newElement.innerHTML = event.data;-- //morphdom(component, newElement, { childrenOnly: true }); }; } +function handleServerError(component, error) {+ if (debugMode) console.error('Server error:', error);++ // Dispatch custom event for application-level error handling+ var errorEvent = new CustomEvent('ssc:error', {+ bubbles: true,+ detail: { error: error, component: component }+ });+ component.dispatchEvent(errorEvent);++ // Show error indicator but keep component functional+ var existingIndicator = component.querySelector('.ssc-error-indicator');+ if (!existingIndicator) {+ var indicator = document.createElement('div');+ indicator.className = 'ssc-error-indicator';+ indicator.style.cssText = 'background: #fee2e2; color: #991b1b; padding: 8px 12px; border-radius: 4px; margin-bottom: 8px; font-size: 14px;';+ indicator.textContent = 'Error: ' + (error.errorMessage || 'An error occurred');+ component.insertBefore(indicator, component.firstChild);++ // Auto-remove after 5 seconds+ setTimeout(function() {+ if (indicator.parentNode) {+ indicator.remove();+ }+ }, 5000);+ }+}++function scheduleReconnect(component) {+ if (component.reconnectAttempts >= SSCConfig.maxReconnectAttempts) {+ if (debugMode) console.log('Max reconnect attempts reached');+ component.connectionState = ConnectionState.FAILED;+ updateConnectionUI(component, ConnectionState.FAILED);+ return;+ }++ component.connectionState = ConnectionState.RECONNECTING;+ component.reconnectAttempts++;++ var delay = Math.min(+ SSCConfig.initialReconnectDelay * Math.pow(SSCConfig.reconnectBackoffMultiplier, component.reconnectAttempts - 1),+ SSCConfig.maxReconnectDelay+ );++ if (debugMode) console.log('Scheduling reconnect attempt', component.reconnectAttempts, 'in', delay, 'ms');+ updateConnectionUI(component, ConnectionState.RECONNECTING);++ component.reconnectTimeout = setTimeout(function() {+ reconnectSSC(component);+ }, delay);+}++function reconnectSSC(component) {+ if (component.reconnectTimeout) {+ clearTimeout(component.reconnectTimeout);+ component.reconnectTimeout = null;+ }++ if (component.connection) {+ // Clean up old connection+ component.connection.onclose = null;+ component.connection.onerror = null;+ component.connection.onmessage = null;+ if (component.connection.readyState === WebSocket.OPEN) {+ component.connection.close();+ }+ }++ connectSSC(component);+}++function handleConnectionFailure(component) {+ component.connectionState = ConnectionState.FAILED;+ updateConnectionUI(component, ConnectionState.FAILED);+}++function updateConnectionUI(component, state) {+ // Remove any existing connection indicator+ var existingIndicator = component.querySelector('.ssc-connection-indicator');+ if (existingIndicator) {+ existingIndicator.remove();+ }++ // Only show indicator for non-connected states+ if (state === ConnectionState.CONNECTED) {+ return;+ }++ var indicator = document.createElement('div');+ indicator.className = 'ssc-connection-indicator';++ switch (state) {+ case ConnectionState.CONNECTING:+ indicator.style.cssText = 'background: #fef3c7; color: #92400e; padding: 8px 12px; border-radius: 4px; margin-bottom: 8px; font-size: 14px;';+ indicator.textContent = 'Connecting...';+ break;+ case ConnectionState.RECONNECTING:+ indicator.style.cssText = 'background: #fef3c7; color: #92400e; padding: 8px 12px; border-radius: 4px; margin-bottom: 8px; font-size: 14px;';+ indicator.textContent = 'Reconnecting... (attempt ' + component.reconnectAttempts + '/' + SSCConfig.maxReconnectAttempts + ')';+ break;+ case ConnectionState.DISCONNECTED:+ indicator.style.cssText = 'background: #fee2e2; color: #991b1b; padding: 8px 12px; border-radius: 4px; margin-bottom: 8px; font-size: 14px;';+ indicator.textContent = 'Disconnected';+ break;+ case ConnectionState.FAILED:+ indicator.style.cssText = 'background: #fee2e2; color: #991b1b; padding: 8px 12px; border-radius: 4px; margin-bottom: 8px; font-size: 14px;';+ indicator.innerHTML = 'Connection failed. <button onclick="this.parentNode.parentNode.reconnectAttempts = 0; reconnectSSC(this.parentNode.parentNode); this.parentNode.remove();" style="background: #991b1b; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; margin-left: 8px;">Retry</button>';+ break;+ }++ component.insertBefore(indicator, component.firstChild);+}+ // Unescapes a HTML-encoded string. function htmlDecode(input) { var doc = new DOMParser().parseFromString(input, "text/html");@@ -52,10 +248,12 @@ } function evaluatePatch(component, nodeOperations) {- for (var nodeOperation of nodeOperations) {+ // Resolve all paths first, but we need to handle DOM mutations carefully+ // Since operations can change the DOM structure, we resolve paths just before each operation+ for (var i = 0; i < nodeOperations.length; i++) {+ var nodeOperation = nodeOperations[i]; nodeOperation.domNode = resolvePathToNode(component, nodeOperation.path);- }- for (var nodeOperation of nodeOperations) {+ if (debugMode) console.log(nodeOperation); evaluateNodeOperation(component, nodeOperation); }@@ -68,6 +266,8 @@ case 'UpdateNode': return updateNode(component, nodeOperation); case 'DeleteNode': return deleteNode(component, nodeOperation); case 'ReplaceNode': return replaceNode(component, nodeOperation);+ case 'UpdateComment': return updateComment(component, nodeOperation);+ case 'UpdatePreEscapedTextNode': return updatePreEscapedTextNode(component, nodeOperation); default: if (debugMode) console.log('Unhandled type', nodeOperation); }@@ -91,6 +291,20 @@ nodeOperation.domNode.textContent = decodedContent; } +function updateComment(component, nodeOperation) {+ // Comment nodes have nodeValue property for their content+ if (nodeOperation.domNode.nodeType === Node.COMMENT_NODE) {+ nodeOperation.domNode.nodeValue = nodeOperation.comment;+ }+}++function updatePreEscapedTextNode(component, nodeOperation) {+ // PreEscaped text nodes (like script/style content) should be updated as-is+ if (nodeOperation.domNode.nodeType === Node.TEXT_NODE) {+ nodeOperation.domNode.textContent = nodeOperation.textContent;+ }+}+ function updateNode(component, nodeOperation) { for (var attributeOperation of nodeOperation.attributeOperations) { evaluateAttributeOperation(nodeOperation.domNode, attributeOperation);@@ -102,16 +316,34 @@ } function replaceNode(component, nodeOperation) {- nodeOperation.domNode.outerHTML = nodeOperation.newNodeHtml;- if (debugMode) console.log('A#######', nodeOperation.domNode);- if (nodeOperation.domNode.type == Node.ELEMENT_NODE) {- for (var component of nodeOperation.domNode.querySelectorAll('.ihp-ssc')) {- initalizeSSC(component);+ var oldNode = nodeOperation.domNode;+ var parent = oldNode.parentNode;++ // Create a temporary container to parse the new HTML+ var template = document.createElement('template');+ template.innerHTML = nodeOperation.newNodeHtml;+ var newNode = template.content.firstChild;++ // Replace the old node with the new one+ if (parent && newNode) {+ parent.replaceChild(newNode, oldNode);++ // Initialize any nested SSC components in the new node+ if (newNode.nodeType === Node.ELEMENT_NODE) {+ // Check if the new node itself is an SSC component+ if (newNode.classList && newNode.classList.contains('ihp-ssc')) {+ initializeSSC(newNode);+ }+ // Check for nested SSC components+ var nestedComponents = newNode.querySelectorAll('.ihp-ssc');+ for (var i = 0; i < nestedComponents.length; i++) {+ initializeSSC(nestedComponents[i]);+ } } } } -const inputTagnames = ["INPUT", "TEXTAREA"];+var inputTagnames = ["INPUT", "TEXTAREA"]; function evaluateAttributeOperation(domNode, op) { // HtmlDiff passes attribute values as raw values — that is, if the attribute value is a single quote mark it'll be@@ -132,11 +364,20 @@ function resolvePathToNode(component, path) { var current = component; - for (var i = path.length; i--; i > 0) {- var currentPathElement = path[i];+ // Traverse path from end to start (path is stored in reverse order)+ // Fixed: was `for (var i = path.length; i--; i > 0)` which had broken loop condition+ for (var i = path.length - 1; i >= 0; i--) {+ var childIndex = path[i]; - if (current.childNodes[currentPathElement])- current = current.childNodes[currentPathElement];+ if (current.childNodes && current.childNodes[childIndex]) {+ current = current.childNodes[childIndex];+ } else {+ // Path is invalid - return current node and log warning+ if (debugMode) {+ console.warn('Invalid path segment:', childIndex, 'at depth', path.length - 1 - i, 'path:', path);+ }+ break;+ } } return current;@@ -144,13 +385,15 @@ /* Returns the nearest element with a class `.ihp-ssc` */ var findActiveComponent = function (element) {- if (element.classList.contains('ihp-ssc')) {- return element;- }+ // Check for null/undefined FIRST before accessing any properties if (!element) { throw new Error('Could not find nearest .ihp-ssc element'); } + if (element.classList && element.classList.contains('ihp-ssc')) {+ return element;+ }+ return findActiveComponent(element.parentNode); }; @@ -159,3 +402,7 @@ template.innerHTML = string; return template.content; }++// Expose functions globally+window.reconnectSSC = reconnectSSC;+window.initializeSSC = initializeSSC;
@@ -1,4 +0,0 @@-/*! jQuery v3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector | (c) JS IHP and other contributors | jquery.org/license */-!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=Array.isArray(d)))?(e?(e=!1,f=c&&Array.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===r.type(a)},isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=!!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLowerCase())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=this.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,N,e),g(f,c,O,e)):(f++,j.call(a,g(f,c,N,e),g(f,c,O,e),g(f,c,N,c.notifyWith))):(d!==N&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a);-}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S),a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},U=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function V(){this.expando=r.expando+V.uid++}V.uid=1,V.prototype={cache:function(a){var b=a[this.expando];return b||(b={},U(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){Array.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(L)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var W=new V,X=new V,Y=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Z=/[A-Z]/g;function $(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:Y.test(a)?JSON.parse(a):a)}function _(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Z,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=$(c)}catch(e){}X.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return X.hasData(a)||W.hasData(a)},data:function(a,b,c){return X.access(a,b,c)},removeData:function(a,b){X.remove(a,b)},_data:function(a,b,c){return W.access(a,b,c)},_removeData:function(a,b){W.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=X.get(f),1===f.nodeType&&!W.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),_(f,d,e[d])));W.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){X.set(this,a)}):T(this,function(b){var c;if(f&&void 0===b){if(c=X.get(f,a),void 0!==c)return c;if(c=_(f,a),void 0!==c)return c}else this.each(function(){X.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=W.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var aa=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,ba=new RegExp("^(?:([+-])=|)("+aa+")([a-z%]*)$","i"),ca=["Top","Right","Bottom","Left"],da=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},ea=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function fa(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&ba.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var ga={};function ha(a){var b,c=a.ownerDocument,d=a.nodeName,e=ga[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&&(e="block"),ga[d]=e,e)}function ia(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=W.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&da(d)&&(e[f]=ha(d))):"none"!==c&&(e[f]="none",W.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ia(this,!0)},hide:function(){return ia(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){da(this)?r(this).show():r(this).hide()})}});var ja=/^(?:checkbox|radio)$/i,ka=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,"<select multiple='multiple'>","</select>"],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,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c<d;c++)W.set(a[c],"globalEval",!b||W.get(b[c],"globalEval"))}var pa=/<|&#?\w+;/;function qa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(pa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ka.exec(f)||["",""])[1].toLowerCase(),i=ma[h]||ma._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==xa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===xa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&B(this,"input"))return this.click(),!1},_default:function(a){return B(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?va:wa,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:wa,isPropagationStopped:wa,isImmediatePropagationStopped:wa,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=va,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=va,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=va,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&sa.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&ta.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return ya(this,a,b,c,d)},one:function(a,b,c,d){return ya(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=wa),this.each(function(){r.event.remove(this,a,c,b)})}});var za=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/<script|<style|<link/i,Ba=/checked\s*(?:[^=]|=\s*.checked.)/i,Ca=/^true\/(.*)/,Da=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}X.hasData(a)&&(h=X.access(a),i=r.extend({},h),X.set(b,i))}}function Ia(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ja.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ja(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,na(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Ga),l=0;l<i;l++)j=h[l],la.test(j.type||"")&&!W.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Da,""),k))}return a}function Ka(a,b,c){for(var d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(na(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&oa(na(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(za,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d<e;d++)Ia(f[d],g[d]);if(b)if(c)for(f=f||na(a),g=g||na(h),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);else Ha(a,h);return g=na(h,"script"),g.length>0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(na(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ja(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(na(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var La=/^margin/,Ma=new RegExp("^("+aa+")(?!px)[a-z%]+$","i"),Na=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.style.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",ra.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,ra.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Oa(a,b,c){var d,e,f,g,h=a.style;return c=c||Na(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight()&&Ma.test(g)&&La.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Pa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Qa=/^(none|table(?!-c[ea]).+)/,Ra=/^--/,Sa={position:"absolute",visibility:"hidden",display:"block"},Ta={letterSpacing:"0",fontWeight:"400"},Ua=["Webkit","Moz","ms"],Va=d.createElement("div").style;function Wa(a){if(a in Va)return a;var b=a[0].toUpperCase()+a.slice(1),c=Ua.length;while(c--)if(a=Ua[c]+b,a in Va)return a}function Xa(a){var b=r.cssProps[a];return b||(b=r.cssProps[a]=Wa(a)||a),b}function Ya(a,b,c){var d=ba.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Za(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ca[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ca[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ca[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ca[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ca[f]+"Width",!0,e)));return g}function $a(a,b,c){var d,e=Na(a),f=Oa(a,b,e),g="border-box"===r.css(a,"boxSizing",!1,e);return Ma.test(f)?f:(d=g&&(o.boxSizingReliable()||f===a.style[b]),"auto"===f&&(f=a["offset"+b[0].toUpperCase()+b.slice(1)]),f=parseFloat(f)||0,f+Za(a,b,c||(g?"border":"content"),d,e)+"px")}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Oa(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=Ra.test(b),j=a.style;return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:j[b]:(f=typeof c,"string"===f&&(e=ba.exec(c))&&e[1]&&(c=fa(a,b,e),f="number"),null!=c&&c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(j[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i?j.setProperty(b,c):j[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b),i=Ra.test(b);return i||(b=Xa(h)),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Oa(a,b,d)),"normal"===e&&b in Ta&&(e=Ta[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Qa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?$a(a,b,d):ea(a,Sa,function(){return $a(a,b,d)})},set:function(a,c,d){var e,f=d&&Na(a),g=d&&Za(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=ba.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Ya(a,c,g)}}}),r.cssHooks.marginLeft=Pa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Oa(a,"marginLeft"))||a.getBoundingClientRect().left-ea(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ca[d]+b]=f[d]||f[d-2]||f[0];return e}},La.test(a)||(r.cssHooks[a+b].set=Ya)}),r.fn.extend({css:function(a,b){return T(this,function(a,b,c){var d,e,f={},g=0;if(Array.isArray(b)){for(d=Na(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}}),r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var _a,ab=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return T(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?_a:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),_a={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=ab[b]||r.find.attr;ab[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=ab[g],ab[g]=e,e=null!=c(a,b,d)?g:null,ab[g]=f),e}});var bb=/^(?:input|select|textarea|button)$/i,cb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function db(a){var b=a.match(L)||[];return b.join(" ")}function eb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,eb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=eb(c),d=1===c.nodeType&&" "+db(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=db(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,eb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=eb(c),d=1===c.nodeType&&" "+db(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=db(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,eb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=eb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+db(eb(c))+" ").indexOf(b)>-1)return!0;return!1}});var fb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(fb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:db(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!B(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var gb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!gb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,gb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.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(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var hb=/\[\]$/,ib=/\r?\n/g,jb=/^(?:submit|button|image|reset|file)$/i,kb=/^(?:input|select|textarea|keygen)/i;function lb(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||hb.test(a)?d(a,e):lb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d);-});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)lb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)lb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&kb.test(this.nodeName)&&!jb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ib,"\r\n")}}):{name:b.name,value:c.replace(ib,"\r\n")}}).get()}}),r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=C.exec(a),g=!c&&[],f?[b.createElement(f[1])]:(f=qa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),b=f.ownerDocument,c=b.documentElement,e=b.defaultView,{top:d.top+e.pageYOffset-c.clientTop,left:d.left+e.pageXOffset-c.clientLeft}):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),B(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left:b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||ra})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return T(this,function(a,d,e){var f;return r.isWindow(a)?f=a:9===a.nodeType&&(f=a.defaultView),void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Pa(o.pixelPosition,function(a,c){if(c)return c=Oa(a,b),Ma.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return T(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.documentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.holdReady=function(a){a?r.readyWait++:r.ready(!0)},r.isArray=Array.isArray,r.parseJSON=JSON.parse,r.nodeName=B,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var mb=a.jQuery,nb=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=nb),b&&a.jQuery===r&&(a.jQuery=mb),r},b||(a.jQuery=a.$=r),r});
@@ -1,2 +0,0 @@-/*! jQuery v3.5.0 | (c) JS Foundation and other contributors | jquery.org/license */-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),D=function(e,t){return e===t&&(l=!0),0},j={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="<a id='"+S+"'></a><select id='"+S+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0<se(t,C,null,[e]).length},se.contains=function(e,t){return(e.ownerDocument||e)!=C&&T(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=C&&T(e);var n=b.attrHandle[t.toLowerCase()],r=n&&j.call(b.attrHandle,t.toLowerCase())?n(e,t,!E):void 0;return void 0!==r?r:d.attributes||!E?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,n=[],r=0,i=0;if(l=!d.detectDuplicates,u=!d.sortStable&&e.slice(0),e.sort(D),l){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return u=null,e},o=se.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else while(t=e[r++])n+=o(t);return n},(b=se.selectors={cacheLength:50,createPseudo:le,match:G,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(B," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(h,e,t,g,v){var y="nth"!==h.slice(0,3),m="last"!==h.slice(-4),x="of-type"===e;return 1===g&&0===v?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u,l=y!==m?"nextSibling":"previousSibling",c=e.parentNode,f=x&&e.nodeName.toLowerCase(),p=!n&&!x,d=!1;if(c){if(y){while(l){a=e;while(a=a[l])if(x?a.nodeName.toLowerCase()===f:1===a.nodeType)return!1;u=l="only"===h&&!u&&"nextSibling"}return!0}if(u=[m?c.firstChild:c.lastChild],m&&p){d=(s=(r=(i=(o=(a=c)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1])&&r[2],a=s&&c.childNodes[s];while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if(1===a.nodeType&&++d&&a===e){i[h]=[k,s,d];break}}else if(p&&(d=s=(r=(i=(o=(a=e)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1]),!1===d)while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if((x?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++d&&(p&&((i=(o=a[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[k,d]),a===e))break;return(d-=v)===g||d%g==0&&0<=d/g}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[S]?a(o):1<a.length?(t=[e,e,"",o],b.setFilters.hasOwnProperty(e.toLowerCase())?le(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=P(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:le(function(e){var r=[],i=[],s=f(e.replace($,"$1"));return s[S]?le(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:le(function(t){return function(e){return 0<se(t,e).length}}),contains:le(function(t){return t=t.replace(te,ne),function(e){return-1<(e.textContent||o(e)).indexOf(t)}}),lang:le(function(n){return V.test(n||"")||se.error("unsupported lang: "+n),n=n.replace(te,ne).toLowerCase(),function(e){var t;do{if(t=E?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===a},focus:function(e){return e===C.activeElement&&(!C.hasFocus||C.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!b.pseudos.empty(e)},header:function(e){return J.test(e.nodeName)},input:function(e){return Q.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ve(function(){return[0]}),last:ve(function(e,t){return[t-1]}),eq:ve(function(e,t,n){return[n<0?n+t:n]}),even:ve(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=b.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})b.pseudos[e]=de(e);for(e in{submit:!0,reset:!0})b.pseudos[e]=he(e);function me(){}function xe(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function be(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,p=r++;return e.first?function(e,t,n){while(e=e[u])if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[k,p];if(n){while(e=e[u])if((1===e.nodeType||f)&&s(e,t,n))return!0}else while(e=e[u])if(1===e.nodeType||f)if(i=(o=e[S]||(e[S]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===k&&r[1]===p)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Te(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Ce(d,h,g,v,y,e){return v&&!v[S]&&(v=Ce(v)),y&&!y[S]&&(y=Ce(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!d||!e&&h?c:Te(c,s,d,n,r),p=g?y||(e?d:l||v)?[]:t:f;if(g&&g(f,p,n,r),v){i=Te(p,u),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(p[u[o]]=!(f[u[o]]=a))}if(e){if(y||d){if(y){i=[],o=p.length;while(o--)(a=p[o])&&i.push(f[o]=a);y(null,p=[],i,r)}o=p.length;while(o--)(a=p[o])&&-1<(i=y?P(e,a):s[o])&&(e[i]=!(t[i]=a))}}else p=Te(p===t?p.splice(l,p.length):p),y?y(null,t,p,r):H.apply(t,p)})}function Ee(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=be(function(e){return e===i},a,!0),l=be(function(e){return-1<P(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!==w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=b.relative[e[s].type])c=[be(we(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[S]){for(n=++s;n<r;n++)if(b.relative[e[n].type])break;return Ce(1<s&&we(c),1<s&&xe(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace($,"$1"),t,s<n&&Ee(e.slice(s,n)),n<r&&Ee(e=e.slice(n)),n<r&&xe(e))}c.push(t)}return we(c)}return me.prototype=b.filters=b.pseudos,b.setFilters=new me,h=se.tokenize=function(e,t){var n,r,i,o,a,s,u,l=x[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=b.preFilter;while(a){for(o in n&&!(r=_.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=z.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace($," ")}),a=a.slice(n.length)),b.filter)!(r=G[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?se.error(e):x(e,s).slice(0)},f=se.compile=function(e,t){var n,v,y,m,x,r,i=[],o=[],a=A[e+" "];if(!a){t||(t=h(e)),n=t.length;while(n--)(a=Ee(t[n]))[S]?i.push(a):o.push(a);(a=A(e,(v=o,m=0<(y=i).length,x=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],p=w,d=e||x&&b.find.TAG("*",i),h=k+=null==p?1:Math.random()||.1,g=d.length;for(i&&(w=t==C||t||i);l!==g&&null!=(o=d[l]);l++){if(x&&o){a=0,t||o.ownerDocument==C||(T(o),n=!E);while(s=v[a++])if(s(o,t||C,n)){r.push(o);break}i&&(k=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=q.call(r));f=Te(f)}H.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&se.uniqueSort(r)}return i&&(k=h,w=p),c},m?le(r):r))).selector=e}return a},g=se.select=function(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&h(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&E&&b.relative[o[1].type]){if(!(t=(b.find.ID(a.matches[0].replace(te,ne),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=G.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],b.relative[s=a.type])break;if((u=b.find[s])&&(r=u(a.matches[0].replace(te,ne),ee.test(o[0].type)&&ye(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&xe(o)))return H.apply(n,r),n;break}}}return(l||f(e,c))(r,t,!E,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},d.sortStable=S.split("").sort(D).join("")===S,d.detectDuplicates=!!l,T(),d.sortDetached=ce(function(e){return 1&e.compareDocumentPosition(C.createElement("fieldset"))}),ce(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),d.attributes&&ce(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ce(function(e){return null==e.getAttribute("disabled")})||fe(R,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(C);S.find=d,S.expr=d.selectors,S.expr[":"]=S.expr.pseudos,S.uniqueSort=S.unique=d.uniqueSort,S.text=d.getText,S.isXMLDoc=d.isXML,S.contains=d.contains,S.escapeSelector=d.escape;var h=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&S(e).is(n))break;r.push(e)}return r},T=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},k=S.expr.match.needsContext;function A(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var N=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1<i.call(n,e)!==r}):S.filter(n,e,r)}S.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?S.find.matchesSelector(r,e)?[r]:[]:S.find.matches(e,S.grep(t,function(e){return 1===e.nodeType}))},S.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(S(e).filter(function(){for(t=0;t<r;t++)if(S.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)S.find(e,i[t],n);return 1<r?S.uniqueSort(n):n},filter:function(e){return this.pushStack(D(this,e||[],!1))},not:function(e){return this.pushStack(D(this,e||[],!0))},is:function(e){return!!D(this,"string"==typeof e&&k.test(e)?S(e):e||[],!1).length}});var j,q=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(S.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&S(e);if(!k.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&S.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?S.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?i.call(S(e),this[0]):i.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(S.uniqueSort(S.merge(this.get(),S(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),S.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return h(e,"parentNode")},parentsUntil:function(e,t,n){return h(e,"parentNode",n)},next:function(e){return O(e,"nextSibling")},prev:function(e){return O(e,"previousSibling")},nextAll:function(e){return h(e,"nextSibling")},prevAll:function(e){return h(e,"previousSibling")},nextUntil:function(e,t,n){return h(e,"nextSibling",n)},prevUntil:function(e,t,n){return h(e,"previousSibling",n)},siblings:function(e){return T((e.parentNode||{}).firstChild,e)},children:function(e){return T(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(A(e,"template")&&(e=e.content||e),S.merge([],e.childNodes))}},function(r,i){S.fn[r]=function(e,t){var n=S.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=S.filter(t,n)),1<this.length&&(H[r]||S.uniqueSort(n),L.test(r)&&n.reverse()),this.pushStack(n)}});var P=/[^\x20\t\r\n\f]+/g;function R(e){return e}function M(e){throw e}function I(e,t,n,r){var i;try{e&&m(i=e.promise)?i.call(e).done(t).fail(n):e&&m(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}S.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},S.each(e.match(P)||[],function(e,t){n[t]=!0}),n):S.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){S.each(e,function(e,t){m(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==w(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return S.each(arguments,function(e,t){var n;while(-1<(n=S.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<S.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},S.extend({Deferred:function(e){var o=[["notify","progress",S.Callbacks("memory"),S.Callbacks("memory"),2],["resolve","done",S.Callbacks("once memory"),S.Callbacks("once memory"),0,"resolved"],["reject","fail",S.Callbacks("once memory"),S.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return S.Deferred(function(r){S.each(o,function(e,t){var n=m(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&m(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,m(t)?s?t.call(e,l(u,o,R,s),l(u,o,M,s)):(u++,t.call(e,l(u,o,R,s),l(u,o,M,s),l(u,o,R,o.notifyWith))):(a!==R&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){S.Deferred.exceptionHook&&S.Deferred.exceptionHook(e,t.stackTrace),u<=i+1&&(a!==M&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(S.Deferred.getStackHook&&(t.stackTrace=S.Deferred.getStackHook()),C.setTimeout(t))}}return S.Deferred(function(e){o[0][3].add(l(0,e,m(r)?r:R,e.notifyWith)),o[1][3].add(l(0,e,m(t)?t:R)),o[2][3].add(l(0,e,m(n)?n:M))}).promise()},promise:function(e){return null!=e?S.extend(e,a):a}},s={};return S.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=S.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?s.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(I(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||m(i[t]&&i[t].then)))return o.then();while(t--)I(i[t],a(t),o.reject);return o.promise()}});var W=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;S.Deferred.exceptionHook=function(e,t){C.console&&C.console.warn&&e&&W.test(e.name)&&C.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},S.readyException=function(e){C.setTimeout(function(){throw e})};var F=S.Deferred();function B(){E.removeEventListener("DOMContentLoaded",B),C.removeEventListener("load",B),S.ready()}S.fn.ready=function(e){return F.then(e)["catch"](function(e){S.readyException(e)}),this},S.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--S.readyWait:S.isReady)||(S.isReady=!0)!==e&&0<--S.readyWait||F.resolveWith(E,[S])}}),S.ready.then=F.then,"complete"===E.readyState||"loading"!==E.readyState&&!E.documentElement.doScroll?C.setTimeout(S.ready):(E.addEventListener("DOMContentLoaded",B),C.addEventListener("load",B));var $=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===w(n))for(s in i=!0,n)$(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,m(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(S(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},_=/^-ms-/,z=/-([a-z])/g;function U(e,t){return t.toUpperCase()}function X(e){return e.replace(_,"ms-").replace(z,U)}var V=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function G(){this.expando=S.expando+G.uid++}G.uid=1,G.prototype={cache:function(e){var t=e[this.expando];return t||(t=Object.create(null),V(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[X(t)]=n;else for(r in t)i[X(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][X(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(X):(t=X(t))in r?[t]:t.match(P)||[]).length;while(n--)delete r[t[n]]}(void 0===t||S.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!S.isEmptyObject(t)}};var Y=new G,Q=new G,J=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,K=/[A-Z]/g;function Z(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(K,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:J.test(i)?JSON.parse(i):i)}catch(e){}Q.set(e,t,n)}else n=void 0;return n}S.extend({hasData:function(e){return Q.hasData(e)||Y.hasData(e)},data:function(e,t,n){return Q.access(e,t,n)},removeData:function(e,t){Q.remove(e,t)},_data:function(e,t,n){return Y.access(e,t,n)},_removeData:function(e,t){Y.remove(e,t)}}),S.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=Q.get(o),1===o.nodeType&&!Y.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=X(r.slice(5)),Z(o,r,i[r]));Y.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){Q.set(this,n)}):$(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=Q.get(o,n))?t:void 0!==(t=Z(o,n))?t:void 0;this.each(function(){Q.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){Q.remove(this,e)})}}),S.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Y.get(e,t),n&&(!r||Array.isArray(n)?r=Y.access(e,t,S.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=S.queue(e,t),r=n.length,i=n.shift(),o=S._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){S.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Y.get(e,n)||Y.access(e,n,{empty:S.Callbacks("once memory").add(function(){Y.remove(e,[t+"queue",n])})})}}),S.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?S.queue(this[0],t):void 0===n?this:this.each(function(){var e=S.queue(this,t,n);S._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&S.dequeue(this,t)})},dequeue:function(e){return this.each(function(){S.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=S.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=Y.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var ee=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,te=new RegExp("^(?:([+-])=|)("+ee+")([a-z%]*)$","i"),ne=["Top","Right","Bottom","Left"],re=E.documentElement,ie=function(e){return S.contains(e.ownerDocument,e)},oe={composed:!0};re.getRootNode&&(ie=function(e){return S.contains(e.ownerDocument,e)||e.getRootNode(oe)===e.ownerDocument});var ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&ie(e)&&"none"===S.css(e,"display")};function se(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return S.css(e,t,"")},u=s(),l=n&&n[3]||(S.cssNumber[t]?"":"px"),c=e.nodeType&&(S.cssNumber[t]||"px"!==l&&+u)&&te.exec(S.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)S.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,S.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var ue={};function le(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Y.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&ae(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ue[s])||(o=a.body.appendChild(a.createElement(s)),u=S.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ue[s]=u)))):"none"!==n&&(l[c]="none",Y.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}S.fn.extend({show:function(){return le(this,!0)},hide:function(){return le(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?S(this).show():S(this).hide()})}});var ce,fe,pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="<textarea>x</textarea>",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="<option></option>",y.option=!!ce.lastChild;var ge={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,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n<r;n++)Y.set(e[n],"globalEval",!t||Y.get(t[n],"globalEval"))}ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td,y.option||(ge.optgroup=ge.option=[1,"<select multiple='multiple'>","</select>"]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===w(o))S.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+S.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;S.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&-1<S.inArray(o,r))i&&i.push(o);else if(l=ie(o),a=ve(f.appendChild(o),"script"),l&&ye(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}var be=/^key/,we=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Te=/^([^.]*)(?:\.(.+)|)/;function Ce(){return!0}function Ee(){return!1}function Se(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function ke(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)ke(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Ee;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return S().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=S.guid++)),e.each(function(){S.event.add(this,t,i,r,n)})}function Ae(e,i,o){o?(Y.set(e,i,!1),S.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Y.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(S.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Y.set(this,i,r),t=o(this,i),this[i](),r!==(n=Y.get(this,i))||t?Y.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Y.set(this,i,{value:S.event.trigger(S.extend(r[0],S.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,i)&&S.event.add(e,i,Ce)}S.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.get(t);if(V(t)){n.handler&&(n=(o=n).handler,i=o.selector),i&&S.find.matchesSelector(re,i),n.guid||(n.guid=S.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof S&&S.event.triggered!==e.type?S.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(P)||[""]).length;while(l--)d=g=(s=Te.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=S.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=S.event.special[d]||{},c=S.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&S.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),S.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.hasData(e)&&Y.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(P)||[""]).length;while(l--)if(d=g=(s=Te.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=S.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||S.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)S.event.remove(e,d+t[l],n,r,!0);S.isEmptyObject(u)&&Y.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=S.event.fix(e),l=(Y.get(this,"events")||Object.create(null))[u.type]||[],c=S.event.special[u.type]||{};for(s[0]=u,t=1;t<arguments.length;t++)s[t]=arguments[t];if(u.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,u)){a=S.event.handlers.call(this,u,l),t=0;while((i=a[t++])&&!u.isPropagationStopped()){u.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!u.isImmediatePropagationStopped())u.rnamespace&&!1!==o.namespace&&!u.rnamespace.test(o.namespace)||(u.handleObj=o,u.data=o.data,void 0!==(r=((S.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s))&&!1===(u.result=r)&&(u.preventDefault(),u.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,u),u.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<S(i,this).index(l):S.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(S.Event.prototype,t,{enumerable:!0,configurable:!0,get:m(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[S.expando]?e:new S.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Ae(t,"click",Ce),!1},trigger:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Ae(t,"click"),!0},_default:function(e){var t=e.target;return pe.test(t.type)&&t.click&&A(t,"input")&&Y.get(t,"click")||A(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},S.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},S.Event=function(e,t){if(!(this instanceof S.Event))return new S.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?Ce:Ee,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&S.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[S.expando]=!0},S.Event.prototype={constructor:S.Event,isDefaultPrevented:Ee,isPropagationStopped:Ee,isImmediatePropagationStopped:Ee,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=Ce,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=Ce,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=Ce,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},S.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(e){var t=e.button;return null==e.which&&be.test(e.type)?null!=e.charCode?e.charCode:e.keyCode:!e.which&&void 0!==t&&we.test(e.type)?1&t?1:2&t?3:4&t?2:0:e.which}},S.event.addProp),S.each({focus:"focusin",blur:"focusout"},function(e,t){S.event.special[e]={setup:function(){return Ae(this,e,Se),!1},trigger:function(){return Ae(this,e),!0},delegateType:t}}),S.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){S.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||S.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),S.fn.extend({on:function(e,t,n,r){return ke(this,e,t,n,r)},one:function(e,t,n,r){return ke(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,S(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=Ee),this.each(function(){S.event.remove(this,e,n,t)})}});var Ne=/<script|<style|<link/i,De=/checked\s*(?:[^=]|=\s*.checked.)/i,je=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n<r;n++)S.event.add(t,i,s[i][n]);Q.hasData(e)&&(o=Q.access(e),a=S.extend({},o),Q.set(t,a))}}function Pe(n,r,i,o){r=g(r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=m(d);if(h||1<f&&"string"==typeof d&&!y.checkClone&&De.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),Pe(t,r,i,o)});if(f&&(t=(e=xe(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=S.map(ve(e,"script"),Le)).length;c<f;c++)u=e,c!==p&&(u=S.clone(u,!0,!0),s&&S.merge(a,ve(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,S.map(a,He),c=0;c<s;c++)u=a[c],he.test(u.type||"")&&!Y.access(u,"globalEval")&&S.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?S._evalUrl&&!u.noModule&&S._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")},l):b(u.textContent.replace(je,""),u,l))}return n}function Re(e,t,n){for(var r,i=t?S.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||S.cleanData(ve(r)),r.parentNode&&(n&&ie(r)&&ye(ve(r,"script")),r.parentNode.removeChild(r));return e}S.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=ie(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||S.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&pe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ve(e),a=a||ve(c),r=0,i=o.length;r<i;r++)Oe(o[r],a[r]);else Oe(e,c);return 0<(a=ve(c,"script")).length&&ye(a,!f&&ve(e,"script")),c},cleanData:function(e){for(var t,n,r,i=S.event.special,o=0;void 0!==(n=e[o]);o++)if(V(n)){if(t=n[Y.expando]){if(t.events)for(r in t.events)i[r]?S.event.remove(n,r):S.removeEvent(n,r,t.handle);n[Y.expando]=void 0}n[Q.expando]&&(n[Q.expando]=void 0)}}}),S.fn.extend({detach:function(e){return Re(this,e,!0)},remove:function(e){return Re(this,e)},text:function(e){return $(this,function(e){return void 0===e?S.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Pe(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||qe(this,e).appendChild(e)})},prepend:function(){return Pe(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=qe(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Pe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Pe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(S.cleanData(ve(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return S.clone(this,e,t)})},html:function(e){return $(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ne.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=S.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(S.cleanData(ve(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return Pe(this,arguments,function(e){var t=this.parentNode;S.inArray(this,n)<0&&(S.cleanData(ve(this)),t&&t.replaceChild(e,this))},n)}}),S.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){S.fn[e]=function(e){for(var t,n=[],r=S(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),S(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var Me=new RegExp("^("+ee+")(?!px)[a-z%]+$","i"),Ie=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},We=function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r},Fe=new RegExp(ne.join("|"),"i");function Be(e,t,n){var r,i,o,a,s=e.style;return(n=n||Ie(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||ie(e)||(a=S.style(e,t)),!y.pixelBoxStyles()&&Me.test(a)&&Fe.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function $e(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(l){u.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",l.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",re.appendChild(u).appendChild(l);var e=C.getComputedStyle(l);n="1%"!==e.top,s=12===t(e.marginLeft),l.style.right="60%",o=36===t(e.right),r=36===t(e.width),l.style.position="absolute",i=12===t(l.offsetWidth/3),re.removeChild(u),l=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s,u=E.createElement("div"),l=E.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===l.style.backgroundClip,S.extend(y,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),s},scrollboxSize:function(){return e(),i},reliableTrDimensions:function(){var e,t,n,r;return null==a&&(e=E.createElement("table"),t=E.createElement("tr"),n=E.createElement("div"),e.style.cssText="position:absolute;left:-11111px",t.style.height="1px",n.style.height="9px",re.appendChild(e).appendChild(t).appendChild(n),r=C.getComputedStyle(t),a=3<parseInt(r.height),re.removeChild(e)),a}}))}();var _e=["Webkit","Moz","ms"],ze=E.createElement("div").style,Ue={};function Xe(e){var t=S.cssProps[e]||Ue[e];return t||(e in ze?e:Ue[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=_e.length;while(n--)if((e=_e[n]+t)in ze)return e}(e)||e)}var Ve=/^(none|table(?!-c[ea]).+)/,Ge=/^--/,Ye={position:"absolute",visibility:"hidden",display:"block"},Qe={letterSpacing:"0",fontWeight:"400"};function Je(e,t,n){var r=te.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Ke(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=S.css(e,n+ne[a],!0,i)),r?("content"===n&&(u-=S.css(e,"padding"+ne[a],!0,i)),"margin"!==n&&(u-=S.css(e,"border"+ne[a]+"Width",!0,i))):(u+=S.css(e,"padding"+ne[a],!0,i),"padding"!==n?u+=S.css(e,"border"+ne[a]+"Width",!0,i):s+=S.css(e,"border"+ne[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function Ze(e,t,n){var r=Ie(e),i=(!y.boxSizingReliable()||n)&&"border-box"===S.css(e,"boxSizing",!1,r),o=i,a=Be(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(Me.test(a)){if(!n)return a;a="auto"}return(!y.boxSizingReliable()&&i||!y.reliableTrDimensions()&&A(e,"tr")||"auto"===a||!parseFloat(a)&&"inline"===S.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===S.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Ke(e,t,n||(i?"border":"content"),o,r,a)+"px"}function et(e,t,n,r,i){return new et.prototype.init(e,t,n,r,i)}S.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Be(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=X(t),u=Ge.test(t),l=e.style;if(u||(t=Xe(s)),a=S.cssHooks[t]||S.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"===(o=typeof n)&&(i=te.exec(n))&&i[1]&&(n=se(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(S.cssNumber[s]?"":"px")),y.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=X(t);return Ge.test(t)||(t=Xe(s)),(a=S.cssHooks[t]||S.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Be(e,t,r)),"normal"===i&&t in Qe&&(i=Qe[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),S.each(["height","width"],function(e,u){S.cssHooks[u]={get:function(e,t,n){if(t)return!Ve.test(S.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Ze(e,u,n):We(e,Ye,function(){return Ze(e,u,n)})},set:function(e,t,n){var r,i=Ie(e),o=!y.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===S.css(e,"boxSizing",!1,i),s=n?Ke(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-Ke(e,u,"border",!1,i)-.5)),s&&(r=te.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=S.css(e,u)),Je(0,t,s)}}}),S.cssHooks.marginLeft=$e(y.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Be(e,"marginLeft"))||e.getBoundingClientRect().left-We(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),S.each({margin:"",padding:"",border:"Width"},function(i,o){S.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+ne[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(S.cssHooks[i+o].set=Je)}),S.fn.extend({css:function(e,t){return $(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Ie(e),i=t.length;a<i;a++)o[t[a]]=S.css(e,t[a],!1,r);return o}return void 0!==n?S.style(e,t,n):S.css(e,t)},e,t,1<arguments.length)}}),((S.Tween=et).prototype={constructor:et,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||S.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(S.cssNumber[n]?"":"px")},cur:function(){var e=et.propHooks[this.prop];return e&&e.get?e.get(this):et.propHooks._default.get(this)},run:function(e){var t,n=et.propHooks[this.prop];return this.options.duration?this.pos=t=S.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):et.propHooks._default.set(this),this}}).init.prototype=et.prototype,(et.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=S.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){S.fx.step[e.prop]?S.fx.step[e.prop](e):1!==e.elem.nodeType||!S.cssHooks[e.prop]&&null==e.elem.style[Xe(e.prop)]?e.elem[e.prop]=e.now:S.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=et.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},S.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},S.fx=et.prototype.init,S.fx.step={};var tt,nt,rt,it,ot=/^(?:toggle|show|hide)$/,at=/queueHooks$/;function st(){nt&&(!1===E.hidden&&C.requestAnimationFrame?C.requestAnimationFrame(st):C.setTimeout(st,S.fx.interval),S.fx.tick())}function ut(){return C.setTimeout(function(){tt=void 0}),tt=Date.now()}function lt(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=ne[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function ct(e,t,n){for(var r,i=(ft.tweeners[t]||[]).concat(ft.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function ft(o,e,t){var n,a,r=0,i=ft.prefilters.length,s=S.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=tt||ut(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;r<i;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),n<1&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:S.extend({},e),opts:S.extend(!0,{specialEasing:{},easing:S.easing._default},t),originalProperties:e,originalOptions:t,startTime:tt||ut(),duration:t.duration,tweens:[],createTween:function(e,t){var n=S.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;t<n;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for(!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=X(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=S.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing);r<i;r++)if(n=ft.prefilters[r].call(l,o,c,l.opts))return m(n.stop)&&(S._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return S.map(c,ct,l),m(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),S.fx.timer(S.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}S.Animation=S.extend(ft,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return se(n.elem,e,te.exec(t),n),n}]},tweener:function(e,t){m(e)?(t=e,e=["*"]):e=e.match(P);for(var n,r=0,i=e.length;r<i;r++)n=e[r],ft.tweeners[n]=ft.tweeners[n]||[],ft.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),v=Y.get(e,"fxshow");for(r in n.queue||(null==(a=S._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,S.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],ot.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||S.style(e,r)}if((u=!S.isEmptyObject(t))||!S.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=Y.get(e,"display")),"none"===(c=S.css(e,"display"))&&(l?c=l:(le([e],!0),l=e.style.display||l,c=S.css(e,"display"),le([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===S.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=Y.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&le([e],!0),p.done(function(){for(r in g||le([e]),Y.remove(e,"fxshow"),d)S.style(e,r,d[r])})),u=ct(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?ft.prefilters.unshift(e):ft.prefilters.push(e)}}),S.speed=function(e,t,n){var r=e&&"object"==typeof e?S.extend({},e):{complete:n||!n&&t||m(e)&&e,duration:e,easing:n&&t||t&&!m(t)&&t};return S.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in S.fx.speeds?r.duration=S.fx.speeds[r.duration]:r.duration=S.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){m(r.old)&&r.old.call(this),r.queue&&S.dequeue(this,r.queue)},r},S.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=S.isEmptyObject(t),o=S.speed(e,n,r),a=function(){var e=ft(this,S.extend({},t),o);(i||Y.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=S.timers,r=Y.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&at.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||S.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=Y.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=S.timers,o=n?n.length:0;for(t.finish=!0,S.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;e<o;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),S.each(["toggle","show","hide"],function(e,r){var i=S.fn[r];S.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(lt(r,!0),e,t,n)}}),S.each({slideDown:lt("show"),slideUp:lt("hide"),slideToggle:lt("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){S.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),S.timers=[],S.fx.tick=function(){var e,t=0,n=S.timers;for(tt=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||S.fx.stop(),tt=void 0},S.fx.timer=function(e){S.timers.push(e),S.fx.start()},S.fx.interval=13,S.fx.start=function(){nt||(nt=!0,st())},S.fx.stop=function(){nt=null},S.fx.speeds={slow:600,fast:200,_default:400},S.fn.delay=function(r,e){return r=S.fx&&S.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=C.setTimeout(e,r);t.stop=function(){C.clearTimeout(n)}})},rt=E.createElement("input"),it=E.createElement("select").appendChild(E.createElement("option")),rt.type="checkbox",y.checkOn=""!==rt.value,y.optSelected=it.selected,(rt=E.createElement("input")).value="t",rt.type="radio",y.radioValue="t"===rt.value;var pt,dt=S.expr.attrHandle;S.fn.extend({attr:function(e,t){return $(this,S.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){S.removeAttr(this,e)})}}),S.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?S.prop(e,t,n):(1===o&&S.isXMLDoc(e)||(i=S.attrHooks[t.toLowerCase()]||(S.expr.match.bool.test(t)?pt:void 0)),void 0!==n?null===n?void S.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=S.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!y.radioValue&&"radio"===t&&A(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(P);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),pt={set:function(e,t,n){return!1===t?S.removeAttr(e,n):e.setAttribute(n,n),n}},S.each(S.expr.match.bool.source.match(/\w+/g),function(e,t){var a=dt[t]||S.find.attr;dt[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=dt[o],dt[o]=r,r=null!=a(e,t,n)?o:null,dt[o]=i),r}});var ht=/^(?:input|select|textarea|button)$/i,gt=/^(?:a|area)$/i;function vt(e){return(e.match(P)||[]).join(" ")}function yt(e){return e.getAttribute&&e.getAttribute("class")||""}function mt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(P)||[]}S.fn.extend({prop:function(e,t){return $(this,S.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[S.propFix[e]||e]})}}),S.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&S.isXMLDoc(e)||(t=S.propFix[t]||t,i=S.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=S.find.attr(e,"tabindex");return t?parseInt(t,10):ht.test(e.nodeName)||gt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),y.optSelected||(S.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),S.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){S.propFix[this.toLowerCase()]=this}),S.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).addClass(t.call(this,e,yt(this)))});if((e=mt(t)).length)while(n=this[u++])if(i=yt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=e[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).removeClass(t.call(this,e,yt(this)))});if(!arguments.length)return this.attr("class","");if((e=mt(t)).length)while(n=this[u++])if(i=yt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=e[a++])while(-1<r.indexOf(" "+o+" "))r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(i,t){var o=typeof i,a="string"===o||Array.isArray(i);return"boolean"==typeof t&&a?t?this.addClass(i):this.removeClass(i):m(i)?this.each(function(e){S(this).toggleClass(i.call(this,e,yt(this),t),t)}):this.each(function(){var e,t,n,r;if(a){t=0,n=S(this),r=mt(i);while(e=r[t++])n.hasClass(e)?n.removeClass(e):n.addClass(e)}else void 0!==i&&"boolean"!==o||((e=yt(this))&&Y.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===i?"":Y.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+vt(yt(n))+" ").indexOf(t))return!0;return!1}});var xt=/\r/g;S.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=m(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,S(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=S.map(t,function(e){return null==e?"":e+""})),(r=S.valHooks[this.type]||S.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=S.valHooks[t.type]||S.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(xt,""):null==e?"":e:void 0}}),S.extend({valHooks:{option:{get:function(e){var t=S.find.attr(e,"value");return null!=t?t:vt(S.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!A(n.parentNode,"optgroup"))){if(t=S(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=S.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<S.inArray(S.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),S.each(["radio","checkbox"],function(){S.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<S.inArray(S(e).val(),t)}},y.checkOn||(S.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),y.focusin="onfocusin"in C;var bt=/^(?:focusinfocus|focusoutblur)$/,wt=function(e){e.stopPropagation()};S.extend(S.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,p=[n||E],d=v.call(e,"type")?e.type:e,h=v.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||E,3!==n.nodeType&&8!==n.nodeType&&!bt.test(d+S.event.triggered)&&(-1<d.indexOf(".")&&(d=(h=d.split(".")).shift(),h.sort()),u=d.indexOf(":")<0&&"on"+d,(e=e[S.expando]?e:new S.Event(d,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:S.makeArray(t,[e]),c=S.event.special[d]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!x(n)){for(s=c.delegateType||d,bt.test(s+d)||(o=o.parentNode);o;o=o.parentNode)p.push(o),a=o;a===(n.ownerDocument||E)&&p.push(a.defaultView||a.parentWindow||C)}i=0;while((o=p[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||d,(l=(Y.get(o,"events")||Object.create(null))[e.type]&&Y.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&V(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!V(n)||u&&m(n[d])&&!x(n)&&((a=n[u])&&(n[u]=null),S.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,wt),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,wt),S.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=S.extend(new S.Event,n,{type:e,isSimulated:!0});S.event.trigger(r,null,t)}}),S.fn.extend({trigger:function(e,t){return this.each(function(){S.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return S.event.trigger(e,t,n,!0)}}),y.focusin||S.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){S.event.simulate(r,e.target,S.event.fix(e))};S.event.special[r]={setup:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r);t||e.addEventListener(n,i,!0),Y.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r)-1;t?Y.access(e,r,t):(e.removeEventListener(n,i,!0),Y.remove(e,r))}}});var Tt=C.location,Ct={guid:Date.now()},Et=/\?/;S.parseXML=function(e){var t;if(!e||"string"!=typeof e)return null;try{t=(new C.DOMParser).parseFromString(e,"text/xml")}catch(e){t=void 0}return t&&!t.getElementsByTagName("parsererror").length||S.error("Invalid XML: "+e),t};var St=/\[\]$/,kt=/\r?\n/g,At=/^(?:submit|button|image|reset|file)$/i,Nt=/^(?:input|select|textarea|keygen)/i;function Dt(n,e,r,i){var t;if(Array.isArray(e))S.each(e,function(e,t){r||St.test(n)?i(n,t):Dt(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==w(e))i(n,e);else for(t in e)Dt(n+"["+t+"]",e[t],r,i)}S.param=function(e,t){var n,r=[],i=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!S.isPlainObject(e))S.each(e,function(){i(this.name,this.value)});else for(n in e)Dt(n,e[n],t,i);return r.join("&")},S.fn.extend({serialize:function(){return S.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=S.prop(this,"elements");return e?S.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!S(this).is(":disabled")&&Nt.test(this.nodeName)&&!At.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=S(this).val();return null==n?null:Array.isArray(n)?S.map(n,function(e){return{name:t.name,value:e.replace(kt,"\r\n")}}):{name:t.name,value:n.replace(kt,"\r\n")}}).get()}});var jt=/%20/g,qt=/#.*$/,Lt=/([?&])_=[^&]*/,Ht=/^(.*?):[ \t]*([^\r\n]*)$/gm,Ot=/^(?:GET|HEAD)$/,Pt=/^\/\//,Rt={},Mt={},It="*/".concat("*"),Wt=E.createElement("a");function Ft(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(P)||[];if(m(t))while(n=i[r++])"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function Bt(t,i,o,a){var s={},u=t===Mt;function l(e){var r;return s[e]=!0,S.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}return l(i.dataTypes[0])||!s["*"]&&l("*")}function $t(e,t){var n,r,i=S.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&S.extend(!0,e,r),e}Wt.href=Tt.href,S.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Tt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Tt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":It,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"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":S.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?$t($t(e,S.ajaxSettings),t):$t(S.ajaxSettings,e)},ajaxPrefilter:Ft(Rt),ajaxTransport:Ft(Mt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=S.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?S(y):S.event,x=S.Deferred(),b=S.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n){n={};while(t=Ht.exec(p))n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||Tt.href)+"").replace(Pt,Tt.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(P)||[""],null==v.crossDomain){r=E.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Wt.protocol+"//"+Wt.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=S.param(v.data,v.traditional)),Bt(Rt,v,t,T),h)return T;for(i in(g=S.event&&v.global)&&0==S.active++&&S.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Ot.test(v.type),f=v.url.replace(qt,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(jt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(Et.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(Lt,"$1"),o=(Et.test(f)?"&":"?")+"_="+Ct.guid+++o),v.url=f+o),v.ifModified&&(S.lastModified[f]&&T.setRequestHeader("If-Modified-Since",S.lastModified[f]),S.etag[f]&&T.setRequestHeader("If-None-Match",S.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+It+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=Bt(Mt,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0<v.timeout&&(d=C.setTimeout(function(){T.abort("timeout")},v.timeout));try{h=!1,c.send(a,l)}catch(e){if(h)throw e;l(-1,e)}}else l(-1,"No Transport");function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&C.clearTimeout(d),c=void 0,p=r||"",T.readyState=0<e?4:0,i=200<=e&&e<300||304===e,n&&(s=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(v,T,n)),!i&&-1<S.inArray("script",v.dataTypes)&&(v.converters["text script"]=function(){}),s=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(S.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(S.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",e<0&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--S.active||S.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return S.get(e,t,n,"json")},getScript:function(e,t){return S.get(e,void 0,t,"script")}}),S.each(["get","post"],function(e,i){S[i]=function(e,t,n,r){return m(t)&&(r=r||n,n=t,t=void 0),S.ajax(S.extend({url:e,type:i,dataType:r,data:t,success:n},S.isPlainObject(e)&&e))}}),S.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),S._evalUrl=function(e,t,n){return S.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){S.globalEval(e,t,n)}})},S.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=S(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(n){return m(n)?this.each(function(e){S(this).wrapInner(n.call(this,e))}):this.each(function(){var e=S(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=m(t);return this.each(function(e){S(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){S(this).replaceWith(this.childNodes)}),this}}),S.expr.pseudos.hidden=function(e){return!S.expr.pseudos.visible(e)},S.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},S.ajaxSettings.xhr=function(){try{return new C.XMLHttpRequest}catch(e){}};var _t={0:200,1223:204},zt=S.ajaxSettings.xhr();y.cors=!!zt&&"withCredentials"in zt,y.ajax=zt=!!zt,S.ajaxTransport(function(i){var o,a;if(y.cors||zt&&!i.crossDomain)return{send:function(e,t){var n,r=i.xhr();if(r.open(i.type,i.url,i.async,i.username,i.password),i.xhrFields)for(n in i.xhrFields)r[n]=i.xhrFields[n];for(n in i.mimeType&&r.overrideMimeType&&r.overrideMimeType(i.mimeType),i.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)r.setRequestHeader(n,e[n]);o=function(e){return function(){o&&(o=a=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===e?r.abort():"error"===e?"number"!=typeof r.status?t(0,"error"):t(r.status,r.statusText):t(_t[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=o(),a=r.onerror=r.ontimeout=o("error"),void 0!==r.onabort?r.onabort=a:r.onreadystatechange=function(){4===r.readyState&&C.setTimeout(function(){o&&a()})},o=o("abort");try{r.send(i.hasContent&&i.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),S.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),S.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return S.globalEval(e),e}}}),S.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),S.ajaxTransport("script",function(n){var r,i;if(n.crossDomain||n.scriptAttrs)return{send:function(e,t){r=S("<script>").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1<s&&(r=vt(e.slice(s)),e=e.slice(0,s)),m(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0<a.length&&S.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?S("<div>").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.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(e,n){S.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}});var Gt=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;S.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),m(e))return r=s.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(s.call(arguments)))}).guid=e.guid=e.guid||S.guid++,i},S.holdReady=function(e){e?S.readyWait++:S.ready(!0)},S.isArray=Array.isArray,S.parseJSON=JSON.parse,S.nodeName=A,S.isFunction=m,S.isWindow=x,S.camelCase=X,S.type=w,S.now=Date.now,S.isNumeric=function(e){var t=S.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},S.trim=function(e){return null==e?"":(e+"").replace(Gt,"")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return S});var Yt=C.jQuery,Qt=C.$;return S.noConflict=function(e){return C.$===S&&(C.$=Qt),e&&C.jQuery===S&&(C.jQuery=Yt),S},"undefined"==typeof e&&(C.jQuery=C.$=S),S});
@@ -1,2 +0,0 @@-/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}S.fn=S.prototype={jquery:f,constructor:S,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(n){return this.pushStack(S.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(S.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},S.extend=S.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||m(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(S.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||S.isPlainObject(n)?n:{},i=!1,a[t]=S.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},S.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=v.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){b(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(p(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(p(Object(e))?S.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(p(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return g(a)},guid:1,support:y}),"function"==typeof Symbol&&(S.fn[Symbol.iterator]=t[Symbol.iterator]),S.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var d=function(n){var e,d,b,o,i,h,f,g,w,u,l,T,C,a,E,v,s,c,y,S="sizzle"+1*new Date,p=n.document,k=0,r=0,m=ue(),x=ue(),A=ue(),N=ue(),j=function(e,t){return e===t&&(l=!0),0},D={}.hasOwnProperty,t=[],q=t.pop,L=t.push,H=t.push,O=t.slice,P=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",I="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",W="\\["+M+"*("+I+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+I+"))|)"+M+"*\\]",F=":("+I+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+W+")*)|.*)\\)|)",B=new RegExp(M+"+","g"),$=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=new RegExp("^"+M+"*,"+M+"*"),z=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="<a id='"+S+"'></a><select id='"+S+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0<se(t,C,null,[e]).length},se.contains=function(e,t){return(e.ownerDocument||e)!=C&&T(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=C&&T(e);var n=b.attrHandle[t.toLowerCase()],r=n&&D.call(b.attrHandle,t.toLowerCase())?n(e,t,!E):void 0;return void 0!==r?r:d.attributes||!E?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,n=[],r=0,i=0;if(l=!d.detectDuplicates,u=!d.sortStable&&e.slice(0),e.sort(j),l){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return u=null,e},o=se.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else while(t=e[r++])n+=o(t);return n},(b=se.selectors={cacheLength:50,createPseudo:le,match:G,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(B," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(h,e,t,g,v){var y="nth"!==h.slice(0,3),m="last"!==h.slice(-4),x="of-type"===e;return 1===g&&0===v?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u,l=y!==m?"nextSibling":"previousSibling",c=e.parentNode,f=x&&e.nodeName.toLowerCase(),p=!n&&!x,d=!1;if(c){if(y){while(l){a=e;while(a=a[l])if(x?a.nodeName.toLowerCase()===f:1===a.nodeType)return!1;u=l="only"===h&&!u&&"nextSibling"}return!0}if(u=[m?c.firstChild:c.lastChild],m&&p){d=(s=(r=(i=(o=(a=c)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1])&&r[2],a=s&&c.childNodes[s];while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if(1===a.nodeType&&++d&&a===e){i[h]=[k,s,d];break}}else if(p&&(d=s=(r=(i=(o=(a=e)[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===k&&r[1]),!1===d)while(a=++s&&a&&a[l]||(d=s=0)||u.pop())if((x?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++d&&(p&&((i=(o=a[S]||(a[S]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[k,d]),a===e))break;return(d-=v)===g||d%g==0&&0<=d/g}}},PSEUDO:function(e,o){var t,a=b.pseudos[e]||b.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[S]?a(o):1<a.length?(t=[e,e,"",o],b.setFilters.hasOwnProperty(e.toLowerCase())?le(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=P(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:le(function(e){var r=[],i=[],s=f(e.replace($,"$1"));return s[S]?le(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:le(function(t){return function(e){return 0<se(t,e).length}}),contains:le(function(t){return t=t.replace(te,ne),function(e){return-1<(e.textContent||o(e)).indexOf(t)}}),lang:le(function(n){return V.test(n||"")||se.error("unsupported lang: "+n),n=n.replace(te,ne).toLowerCase(),function(e){var t;do{if(t=E?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===a},focus:function(e){return e===C.activeElement&&(!C.hasFocus||C.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!b.pseudos.empty(e)},header:function(e){return J.test(e.nodeName)},input:function(e){return Q.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ve(function(){return[0]}),last:ve(function(e,t){return[t-1]}),eq:ve(function(e,t,n){return[n<0?n+t:n]}),even:ve(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=b.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})b.pseudos[e]=de(e);for(e in{submit:!0,reset:!0})b.pseudos[e]=he(e);function me(){}function xe(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function be(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,p=r++;return e.first?function(e,t,n){while(e=e[u])if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[k,p];if(n){while(e=e[u])if((1===e.nodeType||f)&&s(e,t,n))return!0}else while(e=e[u])if(1===e.nodeType||f)if(i=(o=e[S]||(e[S]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===k&&r[1]===p)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Te(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Ce(d,h,g,v,y,e){return v&&!v[S]&&(v=Ce(v)),y&&!y[S]&&(y=Ce(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!d||!e&&h?c:Te(c,s,d,n,r),p=g?y||(e?d:l||v)?[]:t:f;if(g&&g(f,p,n,r),v){i=Te(p,u),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(p[u[o]]=!(f[u[o]]=a))}if(e){if(y||d){if(y){i=[],o=p.length;while(o--)(a=p[o])&&i.push(f[o]=a);y(null,p=[],i,r)}o=p.length;while(o--)(a=p[o])&&-1<(i=y?P(e,a):s[o])&&(e[i]=!(t[i]=a))}}else p=Te(p===t?p.splice(l,p.length):p),y?y(null,t,p,r):H.apply(t,p)})}function Ee(e){for(var i,t,n,r=e.length,o=b.relative[e[0].type],a=o||b.relative[" "],s=o?1:0,u=be(function(e){return e===i},a,!0),l=be(function(e){return-1<P(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!==w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=b.relative[e[s].type])c=[be(we(c),t)];else{if((t=b.filter[e[s].type].apply(null,e[s].matches))[S]){for(n=++s;n<r;n++)if(b.relative[e[n].type])break;return Ce(1<s&&we(c),1<s&&xe(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace($,"$1"),t,s<n&&Ee(e.slice(s,n)),n<r&&Ee(e=e.slice(n)),n<r&&xe(e))}c.push(t)}return we(c)}return me.prototype=b.filters=b.pseudos,b.setFilters=new me,h=se.tokenize=function(e,t){var n,r,i,o,a,s,u,l=x[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=b.preFilter;while(a){for(o in n&&!(r=_.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=z.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace($," ")}),a=a.slice(n.length)),b.filter)!(r=G[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?se.error(e):x(e,s).slice(0)},f=se.compile=function(e,t){var n,v,y,m,x,r,i=[],o=[],a=A[e+" "];if(!a){t||(t=h(e)),n=t.length;while(n--)(a=Ee(t[n]))[S]?i.push(a):o.push(a);(a=A(e,(v=o,m=0<(y=i).length,x=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],p=w,d=e||x&&b.find.TAG("*",i),h=k+=null==p?1:Math.random()||.1,g=d.length;for(i&&(w=t==C||t||i);l!==g&&null!=(o=d[l]);l++){if(x&&o){a=0,t||o.ownerDocument==C||(T(o),n=!E);while(s=v[a++])if(s(o,t||C,n)){r.push(o);break}i&&(k=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=q.call(r));f=Te(f)}H.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&se.uniqueSort(r)}return i&&(k=h,w=p),c},m?le(r):r))).selector=e}return a},g=se.select=function(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&h(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&E&&b.relative[o[1].type]){if(!(t=(b.find.ID(a.matches[0].replace(te,ne),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=G.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],b.relative[s=a.type])break;if((u=b.find[s])&&(r=u(a.matches[0].replace(te,ne),ee.test(o[0].type)&&ye(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&xe(o)))return H.apply(n,r),n;break}}}return(l||f(e,c))(r,t,!E,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},d.sortStable=S.split("").sort(j).join("")===S,d.detectDuplicates=!!l,T(),d.sortDetached=ce(function(e){return 1&e.compareDocumentPosition(C.createElement("fieldset"))}),ce(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),d.attributes&&ce(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ce(function(e){return null==e.getAttribute("disabled")})||fe(R,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(C);S.find=d,S.expr=d.selectors,S.expr[":"]=S.expr.pseudos,S.uniqueSort=S.unique=d.uniqueSort,S.text=d.getText,S.isXMLDoc=d.isXML,S.contains=d.contains,S.escapeSelector=d.escape;var h=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&S(e).is(n))break;r.push(e)}return r},T=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},k=S.expr.match.needsContext;function A(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var N=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1<i.call(n,e)!==r}):S.filter(n,e,r)}S.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?S.find.matchesSelector(r,e)?[r]:[]:S.find.matches(e,S.grep(t,function(e){return 1===e.nodeType}))},S.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(S(e).filter(function(){for(t=0;t<r;t++)if(S.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)S.find(e,i[t],n);return 1<r?S.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&k.test(e)?S(e):e||[],!1).length}});var D,q=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(S.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&S(e);if(!k.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&S.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?S.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?i.call(S(e),this[0]):i.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(S.uniqueSort(S.merge(this.get(),S(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),S.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return h(e,"parentNode")},parentsUntil:function(e,t,n){return h(e,"parentNode",n)},next:function(e){return O(e,"nextSibling")},prev:function(e){return O(e,"previousSibling")},nextAll:function(e){return h(e,"nextSibling")},prevAll:function(e){return h(e,"previousSibling")},nextUntil:function(e,t,n){return h(e,"nextSibling",n)},prevUntil:function(e,t,n){return h(e,"previousSibling",n)},siblings:function(e){return T((e.parentNode||{}).firstChild,e)},children:function(e){return T(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(A(e,"template")&&(e=e.content||e),S.merge([],e.childNodes))}},function(r,i){S.fn[r]=function(e,t){var n=S.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=S.filter(t,n)),1<this.length&&(H[r]||S.uniqueSort(n),L.test(r)&&n.reverse()),this.pushStack(n)}});var P=/[^\x20\t\r\n\f]+/g;function R(e){return e}function M(e){throw e}function I(e,t,n,r){var i;try{e&&m(i=e.promise)?i.call(e).done(t).fail(n):e&&m(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}S.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},S.each(e.match(P)||[],function(e,t){n[t]=!0}),n):S.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){S.each(e,function(e,t){m(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==w(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return S.each(arguments,function(e,t){var n;while(-1<(n=S.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<S.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},S.extend({Deferred:function(e){var o=[["notify","progress",S.Callbacks("memory"),S.Callbacks("memory"),2],["resolve","done",S.Callbacks("once memory"),S.Callbacks("once memory"),0,"resolved"],["reject","fail",S.Callbacks("once memory"),S.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return S.Deferred(function(r){S.each(o,function(e,t){var n=m(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&m(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,m(t)?s?t.call(e,l(u,o,R,s),l(u,o,M,s)):(u++,t.call(e,l(u,o,R,s),l(u,o,M,s),l(u,o,R,o.notifyWith))):(a!==R&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){S.Deferred.exceptionHook&&S.Deferred.exceptionHook(e,t.stackTrace),u<=i+1&&(a!==M&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(S.Deferred.getStackHook&&(t.stackTrace=S.Deferred.getStackHook()),C.setTimeout(t))}}return S.Deferred(function(e){o[0][3].add(l(0,e,m(r)?r:R,e.notifyWith)),o[1][3].add(l(0,e,m(t)?t:R)),o[2][3].add(l(0,e,m(n)?n:M))}).promise()},promise:function(e){return null!=e?S.extend(e,a):a}},s={};return S.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=S.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?s.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(I(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||m(i[t]&&i[t].then)))return o.then();while(t--)I(i[t],a(t),o.reject);return o.promise()}});var W=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;S.Deferred.exceptionHook=function(e,t){C.console&&C.console.warn&&e&&W.test(e.name)&&C.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},S.readyException=function(e){C.setTimeout(function(){throw e})};var F=S.Deferred();function B(){E.removeEventListener("DOMContentLoaded",B),C.removeEventListener("load",B),S.ready()}S.fn.ready=function(e){return F.then(e)["catch"](function(e){S.readyException(e)}),this},S.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--S.readyWait:S.isReady)||(S.isReady=!0)!==e&&0<--S.readyWait||F.resolveWith(E,[S])}}),S.ready.then=F.then,"complete"===E.readyState||"loading"!==E.readyState&&!E.documentElement.doScroll?C.setTimeout(S.ready):(E.addEventListener("DOMContentLoaded",B),C.addEventListener("load",B));var $=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===w(n))for(s in i=!0,n)$(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,m(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(S(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},_=/^-ms-/,z=/-([a-z])/g;function U(e,t){return t.toUpperCase()}function X(e){return e.replace(_,"ms-").replace(z,U)}var V=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function G(){this.expando=S.expando+G.uid++}G.uid=1,G.prototype={cache:function(e){var t=e[this.expando];return t||(t={},V(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[X(t)]=n;else for(r in t)i[X(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][X(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(X):(t=X(t))in r?[t]:t.match(P)||[]).length;while(n--)delete r[t[n]]}(void 0===t||S.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!S.isEmptyObject(t)}};var Y=new G,Q=new G,J=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,K=/[A-Z]/g;function Z(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(K,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:J.test(i)?JSON.parse(i):i)}catch(e){}Q.set(e,t,n)}else n=void 0;return n}S.extend({hasData:function(e){return Q.hasData(e)||Y.hasData(e)},data:function(e,t,n){return Q.access(e,t,n)},removeData:function(e,t){Q.remove(e,t)},_data:function(e,t,n){return Y.access(e,t,n)},_removeData:function(e,t){Y.remove(e,t)}}),S.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=Q.get(o),1===o.nodeType&&!Y.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=X(r.slice(5)),Z(o,r,i[r]));Y.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){Q.set(this,n)}):$(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=Q.get(o,n))?t:void 0!==(t=Z(o,n))?t:void 0;this.each(function(){Q.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){Q.remove(this,e)})}}),S.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Y.get(e,t),n&&(!r||Array.isArray(n)?r=Y.access(e,t,S.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=S.queue(e,t),r=n.length,i=n.shift(),o=S._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){S.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Y.get(e,n)||Y.access(e,n,{empty:S.Callbacks("once memory").add(function(){Y.remove(e,[t+"queue",n])})})}}),S.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?S.queue(this[0],t):void 0===n?this:this.each(function(){var e=S.queue(this,t,n);S._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&S.dequeue(this,t)})},dequeue:function(e){return this.each(function(){S.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=S.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=Y.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var ee=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,te=new RegExp("^(?:([+-])=|)("+ee+")([a-z%]*)$","i"),ne=["Top","Right","Bottom","Left"],re=E.documentElement,ie=function(e){return S.contains(e.ownerDocument,e)},oe={composed:!0};re.getRootNode&&(ie=function(e){return S.contains(e.ownerDocument,e)||e.getRootNode(oe)===e.ownerDocument});var ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&ie(e)&&"none"===S.css(e,"display")};function se(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return S.css(e,t,"")},u=s(),l=n&&n[3]||(S.cssNumber[t]?"":"px"),c=e.nodeType&&(S.cssNumber[t]||"px"!==l&&+u)&&te.exec(S.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)S.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,S.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}var ue={};function le(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Y.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&ae(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=ue[s])||(o=a.body.appendChild(a.createElement(s)),u=S.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),ue[s]=u)))):"none"!==n&&(l[c]="none",Y.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}S.fn.extend({show:function(){return le(this,!0)},hide:function(){return le(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?S(this).show():S(this).hide()})}});var ce,fe,pe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="<textarea>x</textarea>",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="<option></option>",y.option=!!ce.lastChild;var ge={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,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n<r;n++)Y.set(e[n],"globalEval",!t||Y.get(t[n],"globalEval"))}ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td,y.option||(ge.optgroup=ge.option=[1,"<select multiple='multiple'>","</select>"]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d<h;d++)if((o=e[d])||0===o)if("object"===w(o))S.merge(p,o.nodeType?[o]:o);else if(me.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=ge[s]||ge._default,a.innerHTML=u[1]+S.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;S.merge(p,a.childNodes),(a=f.firstChild).textContent=""}else p.push(t.createTextNode(o));f.textContent="",d=0;while(o=p[d++])if(r&&-1<S.inArray(o,r))i&&i.push(o);else if(l=ie(o),a=ve(f.appendChild(o),"script"),l&&ye(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}var be=/^([^.]*)(?:\.(.+)|)/;function we(){return!0}function Te(){return!1}function Ce(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ee(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ee(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Te;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return S().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=S.guid++)),e.each(function(){S.event.add(this,t,i,r,n)})}function Se(e,i,o){o?(Y.set(e,i,!1),S.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Y.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(S.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Y.set(this,i,r),t=o(this,i),this[i](),r!==(n=Y.get(this,i))||t?Y.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n&&n.value}else r.length&&(Y.set(this,i,{value:S.event.trigger(S.extend(r[0],S.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,i)&&S.event.add(e,i,we)}S.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.get(t);if(V(t)){n.handler&&(n=(o=n).handler,i=o.selector),i&&S.find.matchesSelector(re,i),n.guid||(n.guid=S.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof S&&S.event.triggered!==e.type?S.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(P)||[""]).length;while(l--)d=g=(s=be.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=S.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=S.event.special[d]||{},c=S.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&S.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),S.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Y.hasData(e)&&Y.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(P)||[""]).length;while(l--)if(d=g=(s=be.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=S.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||S.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)S.event.remove(e,d+t[l],n,r,!0);S.isEmptyObject(u)&&Y.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=S.event.fix(e),l=(Y.get(this,"events")||Object.create(null))[u.type]||[],c=S.event.special[u.type]||{};for(s[0]=u,t=1;t<arguments.length;t++)s[t]=arguments[t];if(u.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,u)){a=S.event.handlers.call(this,u,l),t=0;while((i=a[t++])&&!u.isPropagationStopped()){u.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!u.isImmediatePropagationStopped())u.rnamespace&&!1!==o.namespace&&!u.rnamespace.test(o.namespace)||(u.handleObj=o,u.data=o.data,void 0!==(r=((S.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s))&&!1===(u.result=r)&&(u.preventDefault(),u.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,u),u.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<S(i,this).index(l):S.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(S.Event.prototype,t,{enumerable:!0,configurable:!0,get:m(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[S.expando]?e:new S.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Se(t,"click",we),!1},trigger:function(e){var t=this||e;return pe.test(t.type)&&t.click&&A(t,"input")&&Se(t,"click"),!0},_default:function(e){var t=e.target;return pe.test(t.type)&&t.click&&A(t,"input")&&Y.get(t,"click")||A(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},S.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},S.Event=function(e,t){if(!(this instanceof S.Event))return new S.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?we:Te,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&S.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[S.expando]=!0},S.Event.prototype={constructor:S.Event,isDefaultPrevented:Te,isPropagationStopped:Te,isImmediatePropagationStopped:Te,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=we,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=we,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=we,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},S.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:!0},S.event.addProp),S.each({focus:"focusin",blur:"focusout"},function(e,t){S.event.special[e]={setup:function(){return Se(this,e,Ce),!1},trigger:function(){return Se(this,e),!0},_default:function(){return!0},delegateType:t}}),S.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){S.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||S.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),S.fn.extend({on:function(e,t,n,r){return Ee(this,e,t,n,r)},one:function(e,t,n,r){return Ee(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,S(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=Te),this.each(function(){S.event.remove(this,e,n,t)})}});var ke=/<script|<style|<link/i,Ae=/checked\s*(?:[^=]|=\s*.checked.)/i,Ne=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n<r;n++)S.event.add(t,i,s[i][n]);Q.hasData(e)&&(o=Q.access(e),a=S.extend({},o),Q.set(t,a))}}function He(n,r,i,o){r=g(r);var e,t,a,s,u,l,c=0,f=n.length,p=f-1,d=r[0],h=m(d);if(h||1<f&&"string"==typeof d&&!y.checkClone&&Ae.test(d))return n.each(function(e){var t=n.eq(e);h&&(r[0]=d.call(this,e,t.html())),He(t,r,i,o)});if(f&&(t=(e=xe(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=S.map(ve(e,"script"),De)).length;c<f;c++)u=e,c!==p&&(u=S.clone(u,!0,!0),s&&S.merge(a,ve(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,S.map(a,qe),c=0;c<s;c++)u=a[c],he.test(u.type||"")&&!Y.access(u,"globalEval")&&S.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?S._evalUrl&&!u.noModule&&S._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")},l):b(u.textContent.replace(Ne,""),u,l))}return n}function Oe(e,t,n){for(var r,i=t?S.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||S.cleanData(ve(r)),r.parentNode&&(n&&ie(r)&&ye(ve(r,"script")),r.parentNode.removeChild(r));return e}S.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=ie(e);if(!(y.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||S.isXMLDoc(e)))for(a=ve(c),r=0,i=(o=ve(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&pe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ve(e),a=a||ve(c),r=0,i=o.length;r<i;r++)Le(o[r],a[r]);else Le(e,c);return 0<(a=ve(c,"script")).length&&ye(a,!f&&ve(e,"script")),c},cleanData:function(e){for(var t,n,r,i=S.event.special,o=0;void 0!==(n=e[o]);o++)if(V(n)){if(t=n[Y.expando]){if(t.events)for(r in t.events)i[r]?S.event.remove(n,r):S.removeEvent(n,r,t.handle);n[Y.expando]=void 0}n[Q.expando]&&(n[Q.expando]=void 0)}}}),S.fn.extend({detach:function(e){return Oe(this,e,!0)},remove:function(e){return Oe(this,e)},text:function(e){return $(this,function(e){return void 0===e?S.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return He(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||je(this,e).appendChild(e)})},prepend:function(){return He(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=je(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return He(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return He(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(S.cleanData(ve(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return S.clone(this,e,t)})},html:function(e){return $(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!ke.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=S.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(S.cleanData(ve(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return He(this,arguments,function(e){var t=this.parentNode;S.inArray(this,n)<0&&(S.cleanData(ve(this)),t&&t.replaceChild(e,this))},n)}}),S.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){S.fn[e]=function(e){for(var t,n=[],r=S(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),S(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var Pe=new RegExp("^("+ee+")(?!px)[a-z%]+$","i"),Re=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=C),t.getComputedStyle(e)},Me=function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r},Ie=new RegExp(ne.join("|"),"i");function We(e,t,n){var r,i,o,a,s=e.style;return(n=n||Re(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||ie(e)||(a=S.style(e,t)),!y.pixelBoxStyles()&&Pe.test(a)&&Ie.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function Fe(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(l){u.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",l.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",re.appendChild(u).appendChild(l);var e=C.getComputedStyle(l);n="1%"!==e.top,s=12===t(e.marginLeft),l.style.right="60%",o=36===t(e.right),r=36===t(e.width),l.style.position="absolute",i=12===t(l.offsetWidth/3),re.removeChild(u),l=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s,u=E.createElement("div"),l=E.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",y.clearCloneStyle="content-box"===l.style.backgroundClip,S.extend(y,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),s},scrollboxSize:function(){return e(),i},reliableTrDimensions:function(){var e,t,n,r;return null==a&&(e=E.createElement("table"),t=E.createElement("tr"),n=E.createElement("div"),e.style.cssText="position:absolute;left:-11111px;border-collapse:separate",t.style.cssText="border:1px solid",t.style.height="1px",n.style.height="9px",n.style.display="block",re.appendChild(e).appendChild(t).appendChild(n),r=C.getComputedStyle(t),a=parseInt(r.height,10)+parseInt(r.borderTopWidth,10)+parseInt(r.borderBottomWidth,10)===t.offsetHeight,re.removeChild(e)),a}}))}();var Be=["Webkit","Moz","ms"],$e=E.createElement("div").style,_e={};function ze(e){var t=S.cssProps[e]||_e[e];return t||(e in $e?e:_e[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=Be.length;while(n--)if((e=Be[n]+t)in $e)return e}(e)||e)}var Ue=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Ve={position:"absolute",visibility:"hidden",display:"block"},Ge={letterSpacing:"0",fontWeight:"400"};function Ye(e,t,n){var r=te.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Qe(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=S.css(e,n+ne[a],!0,i)),r?("content"===n&&(u-=S.css(e,"padding"+ne[a],!0,i)),"margin"!==n&&(u-=S.css(e,"border"+ne[a]+"Width",!0,i))):(u+=S.css(e,"padding"+ne[a],!0,i),"padding"!==n?u+=S.css(e,"border"+ne[a]+"Width",!0,i):s+=S.css(e,"border"+ne[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function Je(e,t,n){var r=Re(e),i=(!y.boxSizingReliable()||n)&&"border-box"===S.css(e,"boxSizing",!1,r),o=i,a=We(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(Pe.test(a)){if(!n)return a;a="auto"}return(!y.boxSizingReliable()&&i||!y.reliableTrDimensions()&&A(e,"tr")||"auto"===a||!parseFloat(a)&&"inline"===S.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===S.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Qe(e,t,n||(i?"border":"content"),o,r,a)+"px"}function Ke(e,t,n,r,i){return new Ke.prototype.init(e,t,n,r,i)}S.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=We(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=X(t),u=Xe.test(t),l=e.style;if(u||(t=ze(s)),a=S.cssHooks[t]||S.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"===(o=typeof n)&&(i=te.exec(n))&&i[1]&&(n=se(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(S.cssNumber[s]?"":"px")),y.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=X(t);return Xe.test(t)||(t=ze(s)),(a=S.cssHooks[t]||S.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=We(e,t,r)),"normal"===i&&t in Ge&&(i=Ge[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),S.each(["height","width"],function(e,u){S.cssHooks[u]={get:function(e,t,n){if(t)return!Ue.test(S.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Je(e,u,n):Me(e,Ve,function(){return Je(e,u,n)})},set:function(e,t,n){var r,i=Re(e),o=!y.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===S.css(e,"boxSizing",!1,i),s=n?Qe(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-Qe(e,u,"border",!1,i)-.5)),s&&(r=te.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=S.css(e,u)),Ye(0,t,s)}}}),S.cssHooks.marginLeft=Fe(y.reliableMarginLeft,function(e,t){if(t)return(parseFloat(We(e,"marginLeft"))||e.getBoundingClientRect().left-Me(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),S.each({margin:"",padding:"",border:"Width"},function(i,o){S.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+ne[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(S.cssHooks[i+o].set=Ye)}),S.fn.extend({css:function(e,t){return $(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Re(e),i=t.length;a<i;a++)o[t[a]]=S.css(e,t[a],!1,r);return o}return void 0!==n?S.style(e,t,n):S.css(e,t)},e,t,1<arguments.length)}}),((S.Tween=Ke).prototype={constructor:Ke,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||S.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(S.cssNumber[n]?"":"px")},cur:function(){var e=Ke.propHooks[this.prop];return e&&e.get?e.get(this):Ke.propHooks._default.get(this)},run:function(e){var t,n=Ke.propHooks[this.prop];return this.options.duration?this.pos=t=S.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Ke.propHooks._default.set(this),this}}).init.prototype=Ke.prototype,(Ke.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=S.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){S.fx.step[e.prop]?S.fx.step[e.prop](e):1!==e.elem.nodeType||!S.cssHooks[e.prop]&&null==e.elem.style[ze(e.prop)]?e.elem[e.prop]=e.now:S.style(e.elem,e.prop,e.now+e.unit)}}}).scrollTop=Ke.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},S.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},S.fx=Ke.prototype.init,S.fx.step={};var Ze,et,tt,nt,rt=/^(?:toggle|show|hide)$/,it=/queueHooks$/;function ot(){et&&(!1===E.hidden&&C.requestAnimationFrame?C.requestAnimationFrame(ot):C.setTimeout(ot,S.fx.interval),S.fx.tick())}function at(){return C.setTimeout(function(){Ze=void 0}),Ze=Date.now()}function st(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=ne[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function ut(e,t,n){for(var r,i=(lt.tweeners[t]||[]).concat(lt.tweeners["*"]),o=0,a=i.length;o<a;o++)if(r=i[o].call(n,t,e))return r}function lt(o,e,t){var n,a,r=0,i=lt.prefilters.length,s=S.Deferred().always(function(){delete u.elem}),u=function(){if(a)return!1;for(var e=Ze||at(),t=Math.max(0,l.startTime+l.duration-e),n=1-(t/l.duration||0),r=0,i=l.tweens.length;r<i;r++)l.tweens[r].run(n);return s.notifyWith(o,[l,n,t]),n<1&&i?t:(i||s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l]),!1)},l=s.promise({elem:o,props:S.extend({},e),opts:S.extend(!0,{specialEasing:{},easing:S.easing._default},t),originalProperties:e,originalOptions:t,startTime:Ze||at(),duration:t.duration,tweens:[],createTween:function(e,t){var n=S.Tween(o,l.opts,e,t,l.opts.specialEasing[e]||l.opts.easing);return l.tweens.push(n),n},stop:function(e){var t=0,n=e?l.tweens.length:0;if(a)return this;for(a=!0;t<n;t++)l.tweens[t].run(1);return e?(s.notifyWith(o,[l,1,0]),s.resolveWith(o,[l,e])):s.rejectWith(o,[l,e]),this}}),c=l.props;for(!function(e,t){var n,r,i,o,a;for(n in e)if(i=t[r=X(n)],o=e[n],Array.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),(a=S.cssHooks[r])&&"expand"in a)for(n in o=a.expand(o),delete e[r],o)n in e||(e[n]=o[n],t[n]=i);else t[r]=i}(c,l.opts.specialEasing);r<i;r++)if(n=lt.prefilters[r].call(l,o,c,l.opts))return m(n.stop)&&(S._queueHooks(l.elem,l.opts.queue).stop=n.stop.bind(n)),n;return S.map(c,ut,l),m(l.opts.start)&&l.opts.start.call(o,l),l.progress(l.opts.progress).done(l.opts.done,l.opts.complete).fail(l.opts.fail).always(l.opts.always),S.fx.timer(S.extend(u,{elem:o,anim:l,queue:l.opts.queue})),l}S.Animation=S.extend(lt,{tweeners:{"*":[function(e,t){var n=this.createTween(e,t);return se(n.elem,e,te.exec(t),n),n}]},tweener:function(e,t){m(e)?(t=e,e=["*"]):e=e.match(P);for(var n,r=0,i=e.length;r<i;r++)n=e[r],lt.tweeners[n]=lt.tweeners[n]||[],lt.tweeners[n].unshift(t)},prefilters:[function(e,t,n){var r,i,o,a,s,u,l,c,f="width"in t||"height"in t,p=this,d={},h=e.style,g=e.nodeType&&ae(e),v=Y.get(e,"fxshow");for(r in n.queue||(null==(a=S._queueHooks(e,"fx")).unqueued&&(a.unqueued=0,s=a.empty.fire,a.empty.fire=function(){a.unqueued||s()}),a.unqueued++,p.always(function(){p.always(function(){a.unqueued--,S.queue(e,"fx").length||a.empty.fire()})})),t)if(i=t[r],rt.test(i)){if(delete t[r],o=o||"toggle"===i,i===(g?"hide":"show")){if("show"!==i||!v||void 0===v[r])continue;g=!0}d[r]=v&&v[r]||S.style(e,r)}if((u=!S.isEmptyObject(t))||!S.isEmptyObject(d))for(r in f&&1===e.nodeType&&(n.overflow=[h.overflow,h.overflowX,h.overflowY],null==(l=v&&v.display)&&(l=Y.get(e,"display")),"none"===(c=S.css(e,"display"))&&(l?c=l:(le([e],!0),l=e.style.display||l,c=S.css(e,"display"),le([e]))),("inline"===c||"inline-block"===c&&null!=l)&&"none"===S.css(e,"float")&&(u||(p.done(function(){h.display=l}),null==l&&(c=h.display,l="none"===c?"":c)),h.display="inline-block")),n.overflow&&(h.overflow="hidden",p.always(function(){h.overflow=n.overflow[0],h.overflowX=n.overflow[1],h.overflowY=n.overflow[2]})),u=!1,d)u||(v?"hidden"in v&&(g=v.hidden):v=Y.access(e,"fxshow",{display:l}),o&&(v.hidden=!g),g&&le([e],!0),p.done(function(){for(r in g||le([e]),Y.remove(e,"fxshow"),d)S.style(e,r,d[r])})),u=ut(g?v[r]:0,r,p),r in v||(v[r]=u.start,g&&(u.end=u.start,u.start=0))}],prefilter:function(e,t){t?lt.prefilters.unshift(e):lt.prefilters.push(e)}}),S.speed=function(e,t,n){var r=e&&"object"==typeof e?S.extend({},e):{complete:n||!n&&t||m(e)&&e,duration:e,easing:n&&t||t&&!m(t)&&t};return S.fx.off?r.duration=0:"number"!=typeof r.duration&&(r.duration in S.fx.speeds?r.duration=S.fx.speeds[r.duration]:r.duration=S.fx.speeds._default),null!=r.queue&&!0!==r.queue||(r.queue="fx"),r.old=r.complete,r.complete=function(){m(r.old)&&r.old.call(this),r.queue&&S.dequeue(this,r.queue)},r},S.fn.extend({fadeTo:function(e,t,n,r){return this.filter(ae).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(t,e,n,r){var i=S.isEmptyObject(t),o=S.speed(e,n,r),a=function(){var e=lt(this,S.extend({},t),o);(i||Y.get(this,"finish"))&&e.stop(!0)};return a.finish=a,i||!1===o.queue?this.each(a):this.queue(o.queue,a)},stop:function(i,e,o){var a=function(e){var t=e.stop;delete e.stop,t(o)};return"string"!=typeof i&&(o=e,e=i,i=void 0),e&&this.queue(i||"fx",[]),this.each(function(){var e=!0,t=null!=i&&i+"queueHooks",n=S.timers,r=Y.get(this);if(t)r[t]&&r[t].stop&&a(r[t]);else for(t in r)r[t]&&r[t].stop&&it.test(t)&&a(r[t]);for(t=n.length;t--;)n[t].elem!==this||null!=i&&n[t].queue!==i||(n[t].anim.stop(o),e=!1,n.splice(t,1));!e&&o||S.dequeue(this,i)})},finish:function(a){return!1!==a&&(a=a||"fx"),this.each(function(){var e,t=Y.get(this),n=t[a+"queue"],r=t[a+"queueHooks"],i=S.timers,o=n?n.length:0;for(t.finish=!0,S.queue(this,a,[]),r&&r.stop&&r.stop.call(this,!0),e=i.length;e--;)i[e].elem===this&&i[e].queue===a&&(i[e].anim.stop(!0),i.splice(e,1));for(e=0;e<o;e++)n[e]&&n[e].finish&&n[e].finish.call(this);delete t.finish})}}),S.each(["toggle","show","hide"],function(e,r){var i=S.fn[r];S.fn[r]=function(e,t,n){return null==e||"boolean"==typeof e?i.apply(this,arguments):this.animate(st(r,!0),e,t,n)}}),S.each({slideDown:st("show"),slideUp:st("hide"),slideToggle:st("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,r){S.fn[e]=function(e,t,n){return this.animate(r,e,t,n)}}),S.timers=[],S.fx.tick=function(){var e,t=0,n=S.timers;for(Ze=Date.now();t<n.length;t++)(e=n[t])()||n[t]!==e||n.splice(t--,1);n.length||S.fx.stop(),Ze=void 0},S.fx.timer=function(e){S.timers.push(e),S.fx.start()},S.fx.interval=13,S.fx.start=function(){et||(et=!0,ot())},S.fx.stop=function(){et=null},S.fx.speeds={slow:600,fast:200,_default:400},S.fn.delay=function(r,e){return r=S.fx&&S.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=C.setTimeout(e,r);t.stop=function(){C.clearTimeout(n)}})},tt=E.createElement("input"),nt=E.createElement("select").appendChild(E.createElement("option")),tt.type="checkbox",y.checkOn=""!==tt.value,y.optSelected=nt.selected,(tt=E.createElement("input")).value="t",tt.type="radio",y.radioValue="t"===tt.value;var ct,ft=S.expr.attrHandle;S.fn.extend({attr:function(e,t){return $(this,S.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){S.removeAttr(this,e)})}}),S.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?S.prop(e,t,n):(1===o&&S.isXMLDoc(e)||(i=S.attrHooks[t.toLowerCase()]||(S.expr.match.bool.test(t)?ct:void 0)),void 0!==n?null===n?void S.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=S.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!y.radioValue&&"radio"===t&&A(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(P);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),ct={set:function(e,t,n){return!1===t?S.removeAttr(e,n):e.setAttribute(n,n),n}},S.each(S.expr.match.bool.source.match(/\w+/g),function(e,t){var a=ft[t]||S.find.attr;ft[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=ft[o],ft[o]=r,r=null!=a(e,t,n)?o:null,ft[o]=i),r}});var pt=/^(?:input|select|textarea|button)$/i,dt=/^(?:a|area)$/i;function ht(e){return(e.match(P)||[]).join(" ")}function gt(e){return e.getAttribute&&e.getAttribute("class")||""}function vt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(P)||[]}S.fn.extend({prop:function(e,t){return $(this,S.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[S.propFix[e]||e]})}}),S.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&S.isXMLDoc(e)||(t=S.propFix[t]||t,i=S.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=S.find.attr(e,"tabindex");return t?parseInt(t,10):pt.test(e.nodeName)||dt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),y.optSelected||(S.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),S.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){S.propFix[this.toLowerCase()]=this}),S.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).addClass(t.call(this,e,gt(this)))});if((e=vt(t)).length)while(n=this[u++])if(i=gt(n),r=1===n.nodeType&&" "+ht(i)+" "){a=0;while(o=e[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=ht(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){S(this).removeClass(t.call(this,e,gt(this)))});if(!arguments.length)return this.attr("class","");if((e=vt(t)).length)while(n=this[u++])if(i=gt(n),r=1===n.nodeType&&" "+ht(i)+" "){a=0;while(o=e[a++])while(-1<r.indexOf(" "+o+" "))r=r.replace(" "+o+" "," ");i!==(s=ht(r))&&n.setAttribute("class",s)}return this},toggleClass:function(i,t){var o=typeof i,a="string"===o||Array.isArray(i);return"boolean"==typeof t&&a?t?this.addClass(i):this.removeClass(i):m(i)?this.each(function(e){S(this).toggleClass(i.call(this,e,gt(this),t),t)}):this.each(function(){var e,t,n,r;if(a){t=0,n=S(this),r=vt(i);while(e=r[t++])n.hasClass(e)?n.removeClass(e):n.addClass(e)}else void 0!==i&&"boolean"!==o||((e=gt(this))&&Y.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===i?"":Y.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+ht(gt(n))+" ").indexOf(t))return!0;return!1}});var yt=/\r/g;S.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=m(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,S(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=S.map(t,function(e){return null==e?"":e+""})),(r=S.valHooks[this.type]||S.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=S.valHooks[t.type]||S.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(yt,""):null==e?"":e:void 0}}),S.extend({valHooks:{option:{get:function(e){var t=S.find.attr(e,"value");return null!=t?t:ht(S.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!A(n.parentNode,"optgroup"))){if(t=S(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=S.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<S.inArray(S.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),S.each(["radio","checkbox"],function(){S.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<S.inArray(S(e).val(),t)}},y.checkOn||(S.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),y.focusin="onfocusin"in C;var mt=/^(?:focusinfocus|focusoutblur)$/,xt=function(e){e.stopPropagation()};S.extend(S.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,p=[n||E],d=v.call(e,"type")?e.type:e,h=v.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||E,3!==n.nodeType&&8!==n.nodeType&&!mt.test(d+S.event.triggered)&&(-1<d.indexOf(".")&&(d=(h=d.split(".")).shift(),h.sort()),u=d.indexOf(":")<0&&"on"+d,(e=e[S.expando]?e:new S.Event(d,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:S.makeArray(t,[e]),c=S.event.special[d]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!x(n)){for(s=c.delegateType||d,mt.test(s+d)||(o=o.parentNode);o;o=o.parentNode)p.push(o),a=o;a===(n.ownerDocument||E)&&p.push(a.defaultView||a.parentWindow||C)}i=0;while((o=p[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||d,(l=(Y.get(o,"events")||Object.create(null))[e.type]&&Y.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&V(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=d,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(p.pop(),t)||!V(n)||u&&m(n[d])&&!x(n)&&((a=n[u])&&(n[u]=null),S.event.triggered=d,e.isPropagationStopped()&&f.addEventListener(d,xt),n[d](),e.isPropagationStopped()&&f.removeEventListener(d,xt),S.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=S.extend(new S.Event,n,{type:e,isSimulated:!0});S.event.trigger(r,null,t)}}),S.fn.extend({trigger:function(e,t){return this.each(function(){S.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return S.event.trigger(e,t,n,!0)}}),y.focusin||S.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){S.event.simulate(r,e.target,S.event.fix(e))};S.event.special[r]={setup:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r);t||e.addEventListener(n,i,!0),Y.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r)-1;t?Y.access(e,r,t):(e.removeEventListener(n,i,!0),Y.remove(e,r))}}});var bt=C.location,wt={guid:Date.now()},Tt=/\?/;S.parseXML=function(e){var t,n;if(!e||"string"!=typeof e)return null;try{t=(new C.DOMParser).parseFromString(e,"text/xml")}catch(e){}return n=t&&t.getElementsByTagName("parsererror")[0],t&&!n||S.error("Invalid XML: "+(n?S.map(n.childNodes,function(e){return e.textContent}).join("\n"):e)),t};var Ct=/\[\]$/,Et=/\r?\n/g,St=/^(?:submit|button|image|reset|file)$/i,kt=/^(?:input|select|textarea|keygen)/i;function At(n,e,r,i){var t;if(Array.isArray(e))S.each(e,function(e,t){r||Ct.test(n)?i(n,t):At(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==w(e))i(n,e);else for(t in e)At(n+"["+t+"]",e[t],r,i)}S.param=function(e,t){var n,r=[],i=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!S.isPlainObject(e))S.each(e,function(){i(this.name,this.value)});else for(n in e)At(n,e[n],t,i);return r.join("&")},S.fn.extend({serialize:function(){return S.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=S.prop(this,"elements");return e?S.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!S(this).is(":disabled")&&kt.test(this.nodeName)&&!St.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=S(this).val();return null==n?null:Array.isArray(n)?S.map(n,function(e){return{name:t.name,value:e.replace(Et,"\r\n")}}):{name:t.name,value:n.replace(Et,"\r\n")}}).get()}});var Nt=/%20/g,jt=/#.*$/,Dt=/([?&])_=[^&]*/,qt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Lt=/^(?:GET|HEAD)$/,Ht=/^\/\//,Ot={},Pt={},Rt="*/".concat("*"),Mt=E.createElement("a");function It(o){return function(e,t){"string"!=typeof e&&(t=e,e="*");var n,r=0,i=e.toLowerCase().match(P)||[];if(m(t))while(n=i[r++])"+"===n[0]?(n=n.slice(1)||"*",(o[n]=o[n]||[]).unshift(t)):(o[n]=o[n]||[]).push(t)}}function Wt(t,i,o,a){var s={},u=t===Pt;function l(e){var r;return s[e]=!0,S.each(t[e]||[],function(e,t){var n=t(i,o,a);return"string"!=typeof n||u||s[n]?u?!(r=n):void 0:(i.dataTypes.unshift(n),l(n),!1)}),r}return l(i.dataTypes[0])||!s["*"]&&l("*")}function Ft(e,t){var n,r,i=S.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&S.extend(!0,e,r),e}Mt.href=bt.href,S.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:bt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(bt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Rt,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"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":S.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Ft(Ft(e,S.ajaxSettings),t):Ft(S.ajaxSettings,e)},ajaxPrefilter:It(Ot),ajaxTransport:It(Pt),ajax:function(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};var c,f,p,n,d,r,h,g,i,o,v=S.ajaxSetup({},t),y=v.context||v,m=v.context&&(y.nodeType||y.jquery)?S(y):S.event,x=S.Deferred(),b=S.Callbacks("once memory"),w=v.statusCode||{},a={},s={},u="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(h){if(!n){n={};while(t=qt.exec(p))n[t[1].toLowerCase()+" "]=(n[t[1].toLowerCase()+" "]||[]).concat(t[2])}t=n[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return h?p:null},setRequestHeader:function(e,t){return null==h&&(e=s[e.toLowerCase()]=s[e.toLowerCase()]||e,a[e]=t),this},overrideMimeType:function(e){return null==h&&(v.mimeType=e),this},statusCode:function(e){var t;if(e)if(h)T.always(e[T.status]);else for(t in e)w[t]=[w[t],e[t]];return this},abort:function(e){var t=e||u;return c&&c.abort(t),l(0,t),this}};if(x.promise(T),v.url=((e||v.url||bt.href)+"").replace(Ht,bt.protocol+"//"),v.type=t.method||t.type||v.method||v.type,v.dataTypes=(v.dataType||"*").toLowerCase().match(P)||[""],null==v.crossDomain){r=E.createElement("a");try{r.href=v.url,r.href=r.href,v.crossDomain=Mt.protocol+"//"+Mt.host!=r.protocol+"//"+r.host}catch(e){v.crossDomain=!0}}if(v.data&&v.processData&&"string"!=typeof v.data&&(v.data=S.param(v.data,v.traditional)),Wt(Ot,v,t,T),h)return T;for(i in(g=S.event&&v.global)&&0==S.active++&&S.event.trigger("ajaxStart"),v.type=v.type.toUpperCase(),v.hasContent=!Lt.test(v.type),f=v.url.replace(jt,""),v.hasContent?v.data&&v.processData&&0===(v.contentType||"").indexOf("application/x-www-form-urlencoded")&&(v.data=v.data.replace(Nt,"+")):(o=v.url.slice(f.length),v.data&&(v.processData||"string"==typeof v.data)&&(f+=(Tt.test(f)?"&":"?")+v.data,delete v.data),!1===v.cache&&(f=f.replace(Dt,"$1"),o=(Tt.test(f)?"&":"?")+"_="+wt.guid+++o),v.url=f+o),v.ifModified&&(S.lastModified[f]&&T.setRequestHeader("If-Modified-Since",S.lastModified[f]),S.etag[f]&&T.setRequestHeader("If-None-Match",S.etag[f])),(v.data&&v.hasContent&&!1!==v.contentType||t.contentType)&&T.setRequestHeader("Content-Type",v.contentType),T.setRequestHeader("Accept",v.dataTypes[0]&&v.accepts[v.dataTypes[0]]?v.accepts[v.dataTypes[0]]+("*"!==v.dataTypes[0]?", "+Rt+"; q=0.01":""):v.accepts["*"]),v.headers)T.setRequestHeader(i,v.headers[i]);if(v.beforeSend&&(!1===v.beforeSend.call(y,T,v)||h))return T.abort();if(u="abort",b.add(v.complete),T.done(v.success),T.fail(v.error),c=Wt(Pt,v,t,T)){if(T.readyState=1,g&&m.trigger("ajaxSend",[T,v]),h)return T;v.async&&0<v.timeout&&(d=C.setTimeout(function(){T.abort("timeout")},v.timeout));try{h=!1,c.send(a,l)}catch(e){if(h)throw e;l(-1,e)}}else l(-1,"No Transport");function l(e,t,n,r){var i,o,a,s,u,l=t;h||(h=!0,d&&C.clearTimeout(d),c=void 0,p=r||"",T.readyState=0<e?4:0,i=200<=e&&e<300||304===e,n&&(s=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(v,T,n)),!i&&-1<S.inArray("script",v.dataTypes)&&S.inArray("json",v.dataTypes)<0&&(v.converters["text script"]=function(){}),s=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}(v,s,T,i),i?(v.ifModified&&((u=T.getResponseHeader("Last-Modified"))&&(S.lastModified[f]=u),(u=T.getResponseHeader("etag"))&&(S.etag[f]=u)),204===e||"HEAD"===v.type?l="nocontent":304===e?l="notmodified":(l=s.state,o=s.data,i=!(a=s.error))):(a=l,!e&&l||(l="error",e<0&&(e=0))),T.status=e,T.statusText=(t||l)+"",i?x.resolveWith(y,[o,l,T]):x.rejectWith(y,[T,l,a]),T.statusCode(w),w=void 0,g&&m.trigger(i?"ajaxSuccess":"ajaxError",[T,v,i?o:a]),b.fireWith(y,[T,l]),g&&(m.trigger("ajaxComplete",[T,v]),--S.active||S.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return S.get(e,t,n,"json")},getScript:function(e,t){return S.get(e,void 0,t,"script")}}),S.each(["get","post"],function(e,i){S[i]=function(e,t,n,r){return m(t)&&(r=r||n,n=t,t=void 0),S.ajax(S.extend({url:e,type:i,dataType:r,data:t,success:n},S.isPlainObject(e)&&e))}}),S.ajaxPrefilter(function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")}),S._evalUrl=function(e,t,n){return S.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){S.globalEval(e,t,n)}})},S.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=S(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(n){return m(n)?this.each(function(e){S(this).wrapInner(n.call(this,e))}):this.each(function(){var e=S(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=m(t);return this.each(function(e){S(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){S(this).replaceWith(this.childNodes)}),this}}),S.expr.pseudos.hidden=function(e){return!S.expr.pseudos.visible(e)},S.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},S.ajaxSettings.xhr=function(){try{return new C.XMLHttpRequest}catch(e){}};var Bt={0:200,1223:204},$t=S.ajaxSettings.xhr();y.cors=!!$t&&"withCredentials"in $t,y.ajax=$t=!!$t,S.ajaxTransport(function(i){var o,a;if(y.cors||$t&&!i.crossDomain)return{send:function(e,t){var n,r=i.xhr();if(r.open(i.type,i.url,i.async,i.username,i.password),i.xhrFields)for(n in i.xhrFields)r[n]=i.xhrFields[n];for(n in i.mimeType&&r.overrideMimeType&&r.overrideMimeType(i.mimeType),i.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest"),e)r.setRequestHeader(n,e[n]);o=function(e){return function(){o&&(o=a=r.onload=r.onerror=r.onabort=r.ontimeout=r.onreadystatechange=null,"abort"===e?r.abort():"error"===e?"number"!=typeof r.status?t(0,"error"):t(r.status,r.statusText):t(Bt[r.status]||r.status,r.statusText,"text"!==(r.responseType||"text")||"string"!=typeof r.responseText?{binary:r.response}:{text:r.responseText},r.getAllResponseHeaders()))}},r.onload=o(),a=r.onerror=r.ontimeout=o("error"),void 0!==r.onabort?r.onabort=a:r.onreadystatechange=function(){4===r.readyState&&C.setTimeout(function(){o&&a()})},o=o("abort");try{r.send(i.hasContent&&i.data||null)}catch(e){if(o)throw e}},abort:function(){o&&o()}}}),S.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),S.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return S.globalEval(e),e}}}),S.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),S.ajaxTransport("script",function(n){var r,i;if(n.crossDomain||n.scriptAttrs)return{send:function(e,t){r=S("<script>").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1<s&&(r=ht(e.slice(s)),e=e.slice(0,s)),m(t)?(n=t,t=void 0):t&&"object"==typeof t&&(i="POST"),0<a.length&&S.ajax({url:e,type:i||"GET",dataType:"html",data:t}).done(function(e){o=arguments,a.html(r?S("<div>").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.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(e,n){S.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}});var Xt=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;S.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),m(e))return r=s.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(s.call(arguments)))}).guid=e.guid=e.guid||S.guid++,i},S.holdReady=function(e){e?S.readyWait++:S.ready(!0)},S.isArray=Array.isArray,S.parseJSON=JSON.parse,S.nodeName=A,S.isFunction=m,S.isWindow=x,S.camelCase=X,S.type=w,S.now=Date.now,S.isNumeric=function(e){var t=S.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},S.trim=function(e){return null==e?"":(e+"").replace(Xt,"")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return S});var Vt=C.jQuery,Gt=C.$;return S.noConflict=function(e){return C.$===S&&(C.$=Gt),e&&C.jQuery===S&&(C.jQuery=Vt),S},"undefined"==typeof e&&(C.jQuery=C.$=S),S});
@@ -1,2 +0,0 @@-/*! jQuery v3.6.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector | (c) OpenJS Foundation and other contributors | jquery.org/license */-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(g,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,v=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),m={},b=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},w=g.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function C(e,t,n){var r,i,o=(n=n||w).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function T(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",E=function(e,t){return new E.fn.init(e,t)};function d(e){var t=!!e&&"length"in e&&e.length,n=T(e);return!b(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0<t&&t-1 in e)}E.fn=E.prototype={jquery:f,constructor:E,length:0,toArray:function(){return s.call(this)},get:function(e){return null==e?s.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=E.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return E.each(this,e)},map:function(n){return this.pushStack(E.map(this,function(e,t){return n.call(e,t,e)}))},slice:function(){return this.pushStack(s.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(E.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(E.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(0<=n&&n<t?[this[n]]:[])},end:function(){return this.prevObject||this.constructor()},push:u,sort:t.sort,splice:t.splice},E.extend=E.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,u=arguments.length,l=!1;for("boolean"==typeof a&&(l=a,a=arguments[s]||{},s++),"object"==typeof a||b(a)||(a={}),s===u&&(a=this,s--);s<u;s++)if(null!=(e=arguments[s]))for(t in e)r=e[t],"__proto__"!==t&&a!==r&&(l&&r&&(E.isPlainObject(r)||(i=Array.isArray(r)))?(n=a[t],o=i&&!Array.isArray(n)?[]:i||E.isPlainObject(n)?n:{},i=!1,a[t]=E.extend(l,o,r)):void 0!==r&&(a[t]=r));return a},E.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isPlainObject:function(e){var t,n;return!(!e||"[object Object]"!==o.call(e))&&(!(t=r(e))||"function"==typeof(n=y.call(t,"constructor")&&t.constructor)&&a.call(n)===l)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},globalEval:function(e,t,n){C(e,{nonce:t&&t.nonce},n)},each:function(e,t){var n,r=0;if(d(e)){for(n=e.length;r<n;r++)if(!1===t.call(e[r],r,e[r]))break}else for(r in e)if(!1===t.call(e[r],r,e[r]))break;return e},makeArray:function(e,t){var n=t||[];return null!=e&&(d(Object(e))?E.merge(n,"string"==typeof e?[e]:e):u.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:i.call(t,e,n)},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;r<n;r++)e[i++]=t[r];return e.length=i,e},grep:function(e,t,n){for(var r=[],i=0,o=e.length,a=!n;i<o;i++)!t(e[i],i)!==a&&r.push(e[i]);return r},map:function(e,t,n){var r,i,o=0,a=[];if(d(e))for(r=e.length;o<r;o++)null!=(i=t(e[o],o,n))&&a.push(i);else for(o in e)null!=(i=t(e[o],o,n))&&a.push(i);return v(a)},guid:1,support:m}),"function"==typeof Symbol&&(E.fn[Symbol.iterator]=t[Symbol.iterator]),E.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(e,t){n["[object "+t+"]"]=t.toLowerCase()});var p=function(n){var e,p,x,o,i,h,f,g,w,u,l,C,T,a,E,v,s,c,y,A="sizzle"+1*new Date,d=n.document,N=0,r=0,m=ue(),b=ue(),S=ue(),k=ue(),D=function(e,t){return e===t&&(l=!0),0},L={}.hasOwnProperty,t=[],j=t.pop,q=t.push,O=t.push,P=t.slice,H=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},I="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",R="[\\x20\\t\\r\\n\\f]",B="(?:\\\\[\\da-fA-F]{1,6}"+R+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",M="\\["+R+"*("+B+")(?:"+R+"*([*^$|!~]?=)"+R+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+B+"))|)"+R+"*\\]",W=":("+B+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",F=new RegExp(R+"+","g"),$=new RegExp("^"+R+"+|((?:^|[^\\\\])(?:\\\\.)*)"+R+"+$","g"),z=new RegExp("^"+R+"*,"+R+"*"),_=new RegExp("^"+R+"*([>+~]|"+R+")"+R+"*"),U=new RegExp(R+"|>"),V=new RegExp(W),X=new RegExp("^"+B+"$"),Q={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),TAG:new RegExp("^("+B+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),bool:new RegExp("^(?:"+I+")$","i"),needsContext:new RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,G=/^(?:input|select|textarea|button)$/i,K=/^h\d$/i,J=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+R+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){C()},ae=xe(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{O.apply(t=P.call(d.childNodes),d.childNodes),t[d.childNodes.length].nodeType}catch(e){O={apply:t.length?function(e,t){q.apply(e,P.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(C(e),e=e||T,E)){if(11!==d&&(u=Z.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return O.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&p.getElementsByClassName&&e.getElementsByClassName)return O.apply(n,e.getElementsByClassName(i)),n}if(p.qsa&&!k[t+" "]&&(!v||!v.test(t))&&(1!==d||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===d&&(U.test(t)||_.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&p.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=A)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+be(l[o]);c=l.join(",")}try{return O.apply(n,f.querySelectorAll(c)),n}catch(e){k(t,!0)}finally{s===A&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[A]=!0,e}function ce(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)x.attrHandle[n[r]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pe(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in p=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},C=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:d;return r!=T&&9===r.nodeType&&r.documentElement&&(a=(T=r).documentElement,E=!i(T),d!=T&&(n=T.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),p.scope=ce(function(e){return a.appendChild(e).appendChild(T.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),p.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),p.getElementsByTagName=ce(function(e){return e.appendChild(T.createComment("")),!e.getElementsByTagName("*").length}),p.getElementsByClassName=J.test(T.getElementsByClassName),p.getById=ce(function(e){return a.appendChild(e).id=A,!T.getElementsByName||!T.getElementsByName(A).length}),p.getById?(x.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=p.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):p.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},x.find.CLASS=p.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(p.qsa=J.test(T.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="<a id='"+A+"'></a><select id='"+A+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+R+"*(?:value|"+I+")"),e.querySelectorAll("[id~="+A+"-]").length||v.push("~="),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+R+"*name"+R+"*="+R+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+A+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=T.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+R+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(p.matchesSelector=J.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){p.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",W)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=J.test(a.compareDocumentPosition),y=t||J.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!p.sortDetached&&t.compareDocumentPosition(e)===n?e==T||e.ownerDocument==d&&y(d,e)?-1:t==T||t.ownerDocument==d&&y(d,t)?1:u?H(u,e)-H(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==T?-1:t==T?1:i?-1:o?1:u?H(u,e)-H(u,t):0;if(i===o)return de(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?de(a[r],s[r]):a[r]==d?-1:s[r]==d?1:0}),T},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(C(e),p.matchesSelector&&E&&!k[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||p.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){k(t,!0)}return 0<se(t,T,null,[e]).length},se.contains=function(e,t){return(e.ownerDocument||e)!=T&&C(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=T&&C(e);var n=x.attrHandle[t.toLowerCase()],r=n&&L.call(x.attrHandle,t.toLowerCase())?n(e,t,!E):void 0;return void 0!==r?r:p.attributes||!E?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,n=[],r=0,i=0;if(l=!p.detectDuplicates,u=!p.sortStable&&e.slice(0),e.sort(D),l){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return u=null,e},o=se.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else while(t=e[r++])n+=o(t);return n},(x=se.selectors={cacheLength:50,createPseudo:le,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&V.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1<t.indexOf(i):"$="===r?i&&t.slice(-i.length)===i:"~="===r?-1<(" "+t.replace(F," ")+" ").indexOf(i):"|="===r&&(t===i||t.slice(0,i.length+1)===i+"-"))}},CHILD:function(h,e,t,g,v){var y="nth"!==h.slice(0,3),m="last"!==h.slice(-4),b="of-type"===e;return 1===g&&0===v?function(e){return!!e.parentNode}:function(e,t,n){var r,i,o,a,s,u,l=y!==m?"nextSibling":"previousSibling",c=e.parentNode,f=b&&e.nodeName.toLowerCase(),d=!n&&!b,p=!1;if(c){if(y){while(l){a=e;while(a=a[l])if(b?a.nodeName.toLowerCase()===f:1===a.nodeType)return!1;u=l="only"===h&&!u&&"nextSibling"}return!0}if(u=[m?c.firstChild:c.lastChild],m&&d){p=(s=(r=(i=(o=(a=c)[A]||(a[A]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===N&&r[1])&&r[2],a=s&&c.childNodes[s];while(a=++s&&a&&a[l]||(p=s=0)||u.pop())if(1===a.nodeType&&++p&&a===e){i[h]=[N,s,p];break}}else if(d&&(p=s=(r=(i=(o=(a=e)[A]||(a[A]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]||[])[0]===N&&r[1]),!1===p)while(a=++s&&a&&a[l]||(p=s=0)||u.pop())if((b?a.nodeName.toLowerCase()===f:1===a.nodeType)&&++p&&(d&&((i=(o=a[A]||(a[A]={}))[a.uniqueID]||(o[a.uniqueID]={}))[h]=[N,p]),a===e))break;return(p-=v)===g||p%g==0&&0<=p/g}}},PSEUDO:function(e,o){var t,a=x.pseudos[e]||x.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return a[A]?a(o):1<a.length?(t=[e,e,"",o],x.setFilters.hasOwnProperty(e.toLowerCase())?le(function(e,t){var n,r=a(e,o),i=r.length;while(i--)e[n=H(e,r[i])]=!(t[n]=r[i])}):function(e){return a(e,0,t)}):a}},pseudos:{not:le(function(e){var r=[],i=[],s=f(e.replace($,"$1"));return s[A]?le(function(e,t,n,r){var i,o=s(e,null,r,[]),a=e.length;while(a--)(i=o[a])&&(e[a]=!(t[a]=i))}):function(e,t,n){return r[0]=e,s(r,null,n,i),r[0]=null,!i.pop()}}),has:le(function(t){return function(e){return 0<se(t,e).length}}),contains:le(function(t){return t=t.replace(te,ne),function(e){return-1<(e.textContent||o(e)).indexOf(t)}}),lang:le(function(n){return X.test(n||"")||se.error("unsupported lang: "+n),n=n.replace(te,ne).toLowerCase(),function(e){var t;do{if(t=E?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=n.location&&n.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===a},focus:function(e){return e===T.activeElement&&(!T.hasFocus||T.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:ge(!1),disabled:ge(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!x.pseudos.empty(e)},header:function(e){return K.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ve(function(){return[0]}),last:ve(function(e,t){return[t-1]}),eq:ve(function(e,t,n){return[n<0?n+t:n]}),even:ve(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:ve(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:ve(function(e,t,n){for(var r=n<0?n+t:t<n?t:n;0<=--r;)e.push(r);return e}),gt:ve(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=x.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})x.pseudos[e]=pe(e);for(e in{submit:!0,reset:!0})x.pseudos[e]=he(e);function me(){}function be(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function xe(s,e,t){var u=e.dir,l=e.next,c=l||u,f=t&&"parentNode"===c,d=r++;return e.first?function(e,t,n){while(e=e[u])if(1===e.nodeType||f)return s(e,t,n);return!1}:function(e,t,n){var r,i,o,a=[N,d];if(n){while(e=e[u])if((1===e.nodeType||f)&&s(e,t,n))return!0}else while(e=e[u])if(1===e.nodeType||f)if(i=(o=e[A]||(e[A]={}))[e.uniqueID]||(o[e.uniqueID]={}),l&&l===e.nodeName.toLowerCase())e=e[u]||e;else{if((r=i[c])&&r[0]===N&&r[1]===d)return a[2]=r[2];if((i[c]=a)[2]=s(e,t,n))return!0}return!1}}function we(i){return 1<i.length?function(e,t,n){var r=i.length;while(r--)if(!i[r](e,t,n))return!1;return!0}:i[0]}function Ce(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s<u;s++)(o=e[s])&&(n&&!n(o,r,i)||(a.push(o),l&&t.push(s)));return a}function Te(p,h,g,v,y,e){return v&&!v[A]&&(v=Te(v)),y&&!y[A]&&(y=Te(y,e)),le(function(e,t,n,r){var i,o,a,s=[],u=[],l=t.length,c=e||function(e,t,n){for(var r=0,i=t.length;r<i;r++)se(e,t[r],n);return n}(h||"*",n.nodeType?[n]:n,[]),f=!p||!e&&h?c:Ce(c,s,p,n,r),d=g?y||(e?p:l||v)?[]:t:f;if(g&&g(f,d,n,r),v){i=Ce(d,u),v(i,[],n,r),o=i.length;while(o--)(a=i[o])&&(d[u[o]]=!(f[u[o]]=a))}if(e){if(y||p){if(y){i=[],o=d.length;while(o--)(a=d[o])&&i.push(f[o]=a);y(null,d=[],i,r)}o=d.length;while(o--)(a=d[o])&&-1<(i=y?H(e,a):s[o])&&(e[i]=!(t[i]=a))}}else d=Ce(d===t?d.splice(l,d.length):d),y?y(null,t,d,r):O.apply(t,d)})}function Ee(e){for(var i,t,n,r=e.length,o=x.relative[e[0].type],a=o||x.relative[" "],s=o?1:0,u=xe(function(e){return e===i},a,!0),l=xe(function(e){return-1<H(i,e)},a,!0),c=[function(e,t,n){var r=!o&&(n||t!==w)||((i=t).nodeType?u(e,t,n):l(e,t,n));return i=null,r}];s<r;s++)if(t=x.relative[e[s].type])c=[xe(we(c),t)];else{if((t=x.filter[e[s].type].apply(null,e[s].matches))[A]){for(n=++s;n<r;n++)if(x.relative[e[n].type])break;return Te(1<s&&we(c),1<s&&be(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace($,"$1"),t,s<n&&Ee(e.slice(s,n)),n<r&&Ee(e=e.slice(n)),n<r&&be(e))}c.push(t)}return we(c)}return me.prototype=x.filters=x.pseudos,x.setFilters=new me,h=se.tokenize=function(e,t){var n,r,i,o,a,s,u,l=b[e+" "];if(l)return t?0:l.slice(0);a=e,s=[],u=x.preFilter;while(a){for(o in n&&!(r=z.exec(a))||(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=_.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace($," ")}),a=a.slice(n.length)),x.filter)!(r=Q[o].exec(a))||u[o]&&!(r=u[o](r))||(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?se.error(e):b(e,s).slice(0)},f=se.compile=function(e,t){var n,v,y,m,b,r,i=[],o=[],a=S[e+" "];if(!a){t||(t=h(e)),n=t.length;while(n--)(a=Ee(t[n]))[A]?i.push(a):o.push(a);(a=S(e,(v=o,m=0<(y=i).length,b=0<v.length,r=function(e,t,n,r,i){var o,a,s,u=0,l="0",c=e&&[],f=[],d=w,p=e||b&&x.find.TAG("*",i),h=N+=null==d?1:Math.random()||.1,g=p.length;for(i&&(w=t==T||t||i);l!==g&&null!=(o=p[l]);l++){if(b&&o){a=0,t||o.ownerDocument==T||(C(o),n=!E);while(s=v[a++])if(s(o,t||T,n)){r.push(o);break}i&&(N=h)}m&&((o=!s&&o)&&u--,e&&c.push(o))}if(u+=l,m&&l!==u){a=0;while(s=y[a++])s(c,f,t,n);if(e){if(0<u)while(l--)c[l]||f[l]||(f[l]=j.call(r));f=Ce(f)}O.apply(r,f),i&&!e&&0<f.length&&1<u+y.length&&se.uniqueSort(r)}return i&&(N=h,w=d),c},m?le(r):r))).selector=e}return a},g=se.select=function(e,t,n,r){var i,o,a,s,u,l="function"==typeof e&&e,c=!r&&h(e=l.selector||e);if(n=n||[],1===c.length){if(2<(o=c[0]=c[0].slice(0)).length&&"ID"===(a=o[0]).type&&9===t.nodeType&&E&&x.relative[o[1].type]){if(!(t=(x.find.ID(a.matches[0].replace(te,ne),t)||[])[0]))return n;l&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=Q.needsContext.test(e)?0:o.length;while(i--){if(a=o[i],x.relative[s=a.type])break;if((u=x.find[s])&&(r=u(a.matches[0].replace(te,ne),ee.test(o[0].type)&&ye(t.parentNode)||t))){if(o.splice(i,1),!(e=r.length&&be(o)))return O.apply(n,r),n;break}}}return(l||f(e,c))(r,t,!E,n,!t||ee.test(e)&&ye(t.parentNode)||t),n},p.sortStable=A.split("").sort(D).join("")===A,p.detectDuplicates=!!l,C(),p.sortDetached=ce(function(e){return 1&e.compareDocumentPosition(T.createElement("fieldset"))}),ce(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),p.attributes&&ce(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ce(function(e){return null==e.getAttribute("disabled")})||fe(I,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),se}(g);E.find=p,E.expr=p.selectors,E.expr[":"]=E.expr.pseudos,E.uniqueSort=E.unique=p.uniqueSort,E.text=p.getText,E.isXMLDoc=p.isXML,E.contains=p.contains,E.escapeSelector=p.escape;var h=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&E(e).is(n))break;r.push(e)}return r},A=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},N=E.expr.match.needsContext;function S(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var k=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return b(n)?E.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?E.grep(e,function(e){return e===n!==r}):"string"!=typeof n?E.grep(e,function(e){return-1<i.call(n,e)!==r}):E.filter(n,e,r)}E.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?E.find.matchesSelector(r,e)?[r]:[]:E.find.matches(e,E.grep(t,function(e){return 1===e.nodeType}))},E.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(E(e).filter(function(){for(t=0;t<r;t++)if(E.contains(i[t],this))return!0}));for(n=this.pushStack([]),t=0;t<r;t++)E.find(e,i[t],n);return 1<r?E.uniqueSort(n):n},filter:function(e){return this.pushStack(D(this,e||[],!1))},not:function(e){return this.pushStack(D(this,e||[],!0))},is:function(e){return!!D(this,"string"==typeof e&&N.test(e)?E(e):e||[],!1).length}});var L,j=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(E.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||L,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:j.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof E?t[0]:t,E.merge(this,E.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:w,!0)),k.test(r[1])&&E.isPlainObject(t))for(r in t)b(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=w.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):b(e)?void 0!==n.ready?n.ready(e):e(E):E.makeArray(e,this)}).prototype=E.fn,L=E(w);var q=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}E.fn.extend({has:function(e){var t=E(e,this),n=t.length;return this.filter(function(){for(var e=0;e<n;e++)if(E.contains(this,t[e]))return!0})},closest:function(e,t){var n,r=0,i=this.length,o=[],a="string"!=typeof e&&E(e);if(!N.test(e))for(;r<i;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?-1<a.index(n):1===n.nodeType&&E.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(1<o.length?E.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?i.call(E(e),this[0]):i.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(E.uniqueSort(E.merge(this.get(),E(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),E.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return h(e,"parentNode")},parentsUntil:function(e,t,n){return h(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return h(e,"nextSibling")},prevAll:function(e){return h(e,"previousSibling")},nextUntil:function(e,t,n){return h(e,"nextSibling",n)},prevUntil:function(e,t,n){return h(e,"previousSibling",n)},siblings:function(e){return A((e.parentNode||{}).firstChild,e)},children:function(e){return A(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(S(e,"template")&&(e=e.content||e),E.merge([],e.childNodes))}},function(r,i){E.fn[r]=function(e,t){var n=E.map(this,i,e);return"Until"!==r.slice(-5)&&(t=e),t&&"string"==typeof t&&(n=E.filter(t,n)),1<this.length&&(O[r]||E.uniqueSort(n),q.test(r)&&n.reverse()),this.pushStack(n)}});var H=/[^\x20\t\r\n\f]+/g;function I(e){return e}function R(e){throw e}function B(e,t,n,r){var i;try{e&&b(i=e.promise)?i.call(e).done(t).fail(n):e&&b(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}E.Callbacks=function(r){var e,n;r="string"==typeof r?(e=r,n={},E.each(e.match(H)||[],function(e,t){n[t]=!0}),n):E.extend({},r);var i,t,o,a,s=[],u=[],l=-1,c=function(){for(a=a||r.once,o=i=!0;u.length;l=-1){t=u.shift();while(++l<s.length)!1===s[l].apply(t[0],t[1])&&r.stopOnFalse&&(l=s.length,t=!1)}r.memory||(t=!1),i=!1,a&&(s=t?[]:"")},f={add:function(){return s&&(t&&!i&&(l=s.length-1,u.push(t)),function n(e){E.each(e,function(e,t){b(t)?r.unique&&f.has(t)||s.push(t):t&&t.length&&"string"!==T(t)&&n(t)})}(arguments),t&&!i&&c()),this},remove:function(){return E.each(arguments,function(e,t){var n;while(-1<(n=E.inArray(t,s,n)))s.splice(n,1),n<=l&&l--}),this},has:function(e){return e?-1<E.inArray(e,s):0<s.length},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=t="",this},disabled:function(){return!s},lock:function(){return a=u=[],t||i||(s=t=""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),i||c()),this},fire:function(){return f.fireWith(this,arguments),this},fired:function(){return!!o}};return f},E.extend({Deferred:function(e){var o=[["notify","progress",E.Callbacks("memory"),E.Callbacks("memory"),2],["resolve","done",E.Callbacks("once memory"),E.Callbacks("once memory"),0,"resolved"],["reject","fail",E.Callbacks("once memory"),E.Callbacks("once memory"),1,"rejected"]],i="pending",a={state:function(){return i},always:function(){return s.done(arguments).fail(arguments),this},"catch":function(e){return a.then(null,e)},pipe:function(){var i=arguments;return E.Deferred(function(r){E.each(o,function(e,t){var n=b(i[t[4]])&&i[t[4]];s[t[1]](function(){var e=n&&n.apply(this,arguments);e&&b(e.promise)?e.promise().progress(r.notify).done(r.resolve).fail(r.reject):r[t[0]+"With"](this,n?[e]:arguments)})}),i=null}).promise()},then:function(t,n,r){var u=0;function l(i,o,a,s){return function(){var n=this,r=arguments,e=function(){var e,t;if(!(i<u)){if((e=a.apply(n,r))===o.promise())throw new TypeError("Thenable self-resolution");t=e&&("object"==typeof e||"function"==typeof e)&&e.then,b(t)?s?t.call(e,l(u,o,I,s),l(u,o,R,s)):(u++,t.call(e,l(u,o,I,s),l(u,o,R,s),l(u,o,I,o.notifyWith))):(a!==I&&(n=void 0,r=[e]),(s||o.resolveWith)(n,r))}},t=s?e:function(){try{e()}catch(e){E.Deferred.exceptionHook&&E.Deferred.exceptionHook(e,t.stackTrace),u<=i+1&&(a!==R&&(n=void 0,r=[e]),o.rejectWith(n,r))}};i?t():(E.Deferred.getStackHook&&(t.stackTrace=E.Deferred.getStackHook()),g.setTimeout(t))}}return E.Deferred(function(e){o[0][3].add(l(0,e,b(r)?r:I,e.notifyWith)),o[1][3].add(l(0,e,b(t)?t:I)),o[2][3].add(l(0,e,b(n)?n:R))}).promise()},promise:function(e){return null!=e?E.extend(e,a):a}},s={};return E.each(o,function(e,t){var n=t[2],r=t[5];a[t[1]]=n.add,r&&n.add(function(){i=r},o[3-e][2].disable,o[3-e][3].disable,o[0][2].lock,o[0][3].lock),n.add(t[3].fire),s[t[0]]=function(){return s[t[0]+"With"](this===s?void 0:this,arguments),this},s[t[0]+"With"]=n.fireWith}),a.promise(s),e&&e.call(s,s),s},when:function(e){var n=arguments.length,t=n,r=Array(t),i=s.call(arguments),o=E.Deferred(),a=function(t){return function(e){r[t]=this,i[t]=1<arguments.length?s.call(arguments):e,--n||o.resolveWith(r,i)}};if(n<=1&&(B(e,o.done(a(t)).resolve,o.reject,!n),"pending"===o.state()||b(i[t]&&i[t].then)))return o.then();while(t--)B(i[t],a(t),o.reject);return o.promise()}});var M=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;E.Deferred.exceptionHook=function(e,t){g.console&&g.console.warn&&e&&M.test(e.name)&&g.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},E.readyException=function(e){g.setTimeout(function(){throw e})};var W=E.Deferred();function F(){w.removeEventListener("DOMContentLoaded",F),g.removeEventListener("load",F),E.ready()}E.fn.ready=function(e){return W.then(e)["catch"](function(e){E.readyException(e)}),this},E.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--E.readyWait:E.isReady)||(E.isReady=!0)!==e&&0<--E.readyWait||W.resolveWith(w,[E])}}),E.ready.then=W.then,"complete"===w.readyState||"loading"!==w.readyState&&!w.documentElement.doScroll?g.setTimeout(E.ready):(w.addEventListener("DOMContentLoaded",F),g.addEventListener("load",F));var $=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===T(n))for(s in i=!0,n)$(e,t,s,n[s],!0,o,a);else if(void 0!==r&&(i=!0,b(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(E(e),n)})),t))for(;s<u;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:l?t.call(e):u?t(e[0],n):o},z=/^-ms-/,_=/-([a-z])/g;function U(e,t){return t.toUpperCase()}function V(e){return e.replace(z,"ms-").replace(_,U)}var X=function(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType};function Q(){this.expando=E.expando+Q.uid++}Q.uid=1,Q.prototype={cache:function(e){var t=e[this.expando];return t||(t={},X(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if("string"==typeof t)i[V(t)]=n;else for(r in t)i[V(r)]=t[r];return i},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][V(t)]},access:function(e,t,n){return void 0===t||t&&"string"==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(V):(t=V(t))in r?[t]:t.match(H)||[]).length;while(n--)delete r[t[n]]}(void 0===t||E.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!E.isEmptyObject(t)}};var Y=new Q,G=new Q,K=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,J=/[A-Z]/g;function Z(e,t,n){var r,i;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(J,"-$&").toLowerCase(),"string"==typeof(n=e.getAttribute(r))){try{n="true"===(i=n)||"false"!==i&&("null"===i?null:i===+i+""?+i:K.test(i)?JSON.parse(i):i)}catch(e){}G.set(e,t,n)}else n=void 0;return n}E.extend({hasData:function(e){return G.hasData(e)||Y.hasData(e)},data:function(e,t,n){return G.access(e,t,n)},removeData:function(e,t){G.remove(e,t)},_data:function(e,t,n){return Y.access(e,t,n)},_removeData:function(e,t){Y.remove(e,t)}}),E.fn.extend({data:function(n,e){var t,r,i,o=this[0],a=o&&o.attributes;if(void 0===n){if(this.length&&(i=G.get(o),1===o.nodeType&&!Y.get(o,"hasDataAttrs"))){t=a.length;while(t--)a[t]&&0===(r=a[t].name).indexOf("data-")&&(r=V(r.slice(5)),Z(o,r,i[r]));Y.set(o,"hasDataAttrs",!0)}return i}return"object"==typeof n?this.each(function(){G.set(this,n)}):$(this,function(e){var t;if(o&&void 0===e)return void 0!==(t=G.get(o,n))?t:void 0!==(t=Z(o,n))?t:void 0;this.each(function(){G.set(this,n,e)})},null,e,1<arguments.length,null,!0)},removeData:function(e){return this.each(function(){G.remove(this,e)})}}),E.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Y.get(e,t),n&&(!r||Array.isArray(n)?r=Y.access(e,t,E.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=E.queue(e,t),r=n.length,i=n.shift(),o=E._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,function(){E.dequeue(e,t)},o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Y.get(e,n)||Y.access(e,n,{empty:E.Callbacks("once memory").add(function(){Y.remove(e,[t+"queue",n])})})}}),E.fn.extend({queue:function(t,n){var e=2;return"string"!=typeof t&&(n=t,t="fx",e--),arguments.length<e?E.queue(this[0],t):void 0===n?this:this.each(function(){var e=E.queue(this,t,n);E._queueHooks(this,t),"fx"===t&&"inprogress"!==e[0]&&E.dequeue(this,t)})},dequeue:function(e){return this.each(function(){E.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=E.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=void 0),e=e||"fx";while(a--)(n=Y.get(o[a],e+"queueHooks"))&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var ee=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,te=new RegExp("^(?:([+-])=|)("+ee+")([a-z%]*)$","i"),ne=["Top","Right","Bottom","Left"],re=w.documentElement,ie=function(e){return E.contains(e.ownerDocument,e)},oe={composed:!0};re.getRootNode&&(ie=function(e){return E.contains(e.ownerDocument,e)||e.getRootNode(oe)===e.ownerDocument});var ae=function(e,t){return"none"===(e=t||e).style.display||""===e.style.display&&ie(e)&&"none"===E.css(e,"display")};var se={};function ue(e,t){for(var n,r,i,o,a,s,u,l=[],c=0,f=e.length;c<f;c++)(r=e[c]).style&&(n=r.style.display,t?("none"===n&&(l[c]=Y.get(r,"display")||null,l[c]||(r.style.display="")),""===r.style.display&&ae(r)&&(l[c]=(u=a=o=void 0,a=(i=r).ownerDocument,s=i.nodeName,(u=se[s])||(o=a.body.appendChild(a.createElement(s)),u=E.css(o,"display"),o.parentNode.removeChild(o),"none"===u&&(u="block"),se[s]=u)))):"none"!==n&&(l[c]="none",Y.set(r,"display",n)));for(c=0;c<f;c++)null!=l[c]&&(e[c].style.display=l[c]);return e}E.fn.extend({show:function(){return ue(this,!0)},hide:function(){return ue(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){ae(this)?E(this).show():E(this).hide()})}});var le,ce,fe=/^(?:checkbox|radio)$/i,de=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i;le=w.createDocumentFragment().appendChild(w.createElement("div")),(ce=w.createElement("input")).setAttribute("type","radio"),ce.setAttribute("checked","checked"),ce.setAttribute("name","t"),le.appendChild(ce),m.checkClone=le.cloneNode(!0).cloneNode(!0).lastChild.checked,le.innerHTML="<textarea>x</textarea>",m.noCloneChecked=!!le.cloneNode(!0).lastChild.defaultValue,le.innerHTML="<option></option>",m.option=!!le.lastChild;var he={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,"",""]};function ge(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&S(e,t)?E.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n<r;n++)Y.set(e[n],"globalEval",!t||Y.get(t[n],"globalEval"))}he.tbody=he.tfoot=he.colgroup=he.caption=he.thead,he.th=he.td,m.option||(he.optgroup=he.option=[1,"<select multiple='multiple'>","</select>"]);var ye=/<|&#?\w+;/;function me(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p<h;p++)if((o=e[p])||0===o)if("object"===T(o))E.merge(d,o.nodeType?[o]:o);else if(ye.test(o)){a=a||f.appendChild(t.createElement("div")),s=(de.exec(o)||["",""])[1].toLowerCase(),u=he[s]||he._default,a.innerHTML=u[1]+E.htmlPrefilter(o)+u[2],c=u[0];while(c--)a=a.lastChild;E.merge(d,a.childNodes),(a=f.firstChild).textContent=""}else d.push(t.createTextNode(o));f.textContent="",p=0;while(o=d[p++])if(r&&-1<E.inArray(o,r))i&&i.push(o);else if(l=ie(o),a=ge(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])pe.test(o.type||"")&&n.push(o)}return f}var be=/^([^.]*)(?:\.(.+)|)/;function xe(){return!0}function we(){return!1}function Ce(e,t){return e===function(){try{return w.activeElement}catch(e){}}()==("focus"===t)}function Te(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Te(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=we;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return E().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=E.guid++)),e.each(function(){E.event.add(this,t,i,r,n)})}function Ee(e,i,o){o?(Y.set(e,i,!1),E.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Y.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(E.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Y.set(this,i,r),t=o(this,i),this[i](),r!==(n=Y.get(this,i))||t?Y.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n&&n.value}else r.length&&(Y.set(this,i,{value:E.event.trigger(E.extend(r[0],E.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,i)&&E.event.add(e,i,xe)}E.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,d,p,h,g,v=Y.get(t);if(X(t)){n.handler&&(n=(o=n).handler,i=o.selector),i&&E.find.matchesSelector(re,i),n.guid||(n.guid=E.guid++),(u=v.events)||(u=v.events=Object.create(null)),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof E&&E.event.triggered!==e.type?E.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(H)||[""]).length;while(l--)p=g=(s=be.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),p&&(f=E.event.special[p]||{},p=(i?f.delegateType:f.bindType)||p,f=E.event.special[p]||{},c=E.extend({type:p,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&E.expr.match.needsContext.test(i),namespace:h.join(".")},o),(d=u[p])||((d=u[p]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(p,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?d.splice(d.delegateCount++,0,c):d.push(c),E.event.global[p]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,d,p,h,g,v=Y.hasData(e)&&Y.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(H)||[""]).length;while(l--)if(p=g=(s=be.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),p){f=E.event.special[p]||{},d=u[p=(r?f.delegateType:f.bindType)||p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=d.length;while(o--)c=d[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(o,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(e,c));a&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||E.removeEvent(e,p,v.handle),delete u[p])}else for(p in u)E.event.remove(e,p+t[l],n,r,!0);E.isEmptyObject(u)&&Y.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=new Array(arguments.length),u=E.event.fix(e),l=(Y.get(this,"events")||Object.create(null))[u.type]||[],c=E.event.special[u.type]||{};for(s[0]=u,t=1;t<arguments.length;t++)s[t]=arguments[t];if(u.delegateTarget=this,!c.preDispatch||!1!==c.preDispatch.call(this,u)){a=E.event.handlers.call(this,u,l),t=0;while((i=a[t++])&&!u.isPropagationStopped()){u.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!u.isImmediatePropagationStopped())u.rnamespace&&!1!==o.namespace&&!u.rnamespace.test(o.namespace)||(u.handleObj=o,u.data=o.data,void 0!==(r=((E.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,s))&&!1===(u.result=r)&&(u.preventDefault(),u.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,u),u.result}},handlers:function(e,t){var n,r,i,o,a,s=[],u=t.delegateCount,l=e.target;if(u&&l.nodeType&&!("click"===e.type&&1<=e.button))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n<u;n++)void 0===a[i=(r=t[n]).selector+" "]&&(a[i]=r.needsContext?-1<E(i,this).index(l):E.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u<t.length&&s.push({elem:l,handlers:t.slice(u)}),s},addProp:function(t,e){Object.defineProperty(E.Event.prototype,t,{enumerable:!0,configurable:!0,get:b(e)?function(){if(this.originalEvent)return e(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[t]},set:function(e){Object.defineProperty(this,t,{enumerable:!0,configurable:!0,writable:!0,value:e})}})},fix:function(e){return e[E.expando]?e:new E.Event(e)},special:{load:{noBubble:!0},click:{setup:function(e){var t=this||e;return fe.test(t.type)&&t.click&&S(t,"input")&&Ee(t,"click",xe),!1},trigger:function(e){var t=this||e;return fe.test(t.type)&&t.click&&S(t,"input")&&Ee(t,"click"),!0},_default:function(e){var t=e.target;return fe.test(t.type)&&t.click&&S(t,"input")&&Y.get(t,"click")||S(t,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}}},E.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n)},E.Event=function(e,t){if(!(this instanceof E.Event))return new E.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&!1===e.returnValue?xe:we,this.target=e.target&&3===e.target.nodeType?e.target.parentNode:e.target,this.currentTarget=e.currentTarget,this.relatedTarget=e.relatedTarget):this.type=e,t&&E.extend(this,t),this.timeStamp=e&&e.timeStamp||Date.now(),this[E.expando]=!0},E.Event.prototype={constructor:E.Event,isDefaultPrevented:we,isPropagationStopped:we,isImmediatePropagationStopped:we,isSimulated:!1,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=xe,e&&!this.isSimulated&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=xe,e&&!this.isSimulated&&e.stopPropagation()},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=xe,e&&!this.isSimulated&&e.stopImmediatePropagation(),this.stopPropagation()}},E.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:!0},E.event.addProp),E.each({focus:"focusin",blur:"focusout"},function(e,t){E.event.special[e]={setup:function(){return Ee(this,e,Ce),!1},trigger:function(){return Ee(this,e),!0},_default:function(){return!0},delegateType:t}}),E.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,i){E.event.special[e]={delegateType:i,bindType:i,handle:function(e){var t,n=e.relatedTarget,r=e.handleObj;return n&&(n===this||E.contains(this,n))||(e.type=r.origType,t=r.handler.apply(this,arguments),e.type=i),t}}}),E.fn.extend({on:function(e,t,n,r){return Te(this,e,t,n,r)},one:function(e,t,n,r){return Te(this,e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,E(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return!1!==t&&"function"!=typeof t||(n=t,t=void 0),!1===n&&(n=we),this.each(function(){E.event.remove(this,e,n,t)})}});var Ae=/<script|<style|<link/i,Ne=/checked\s*(?:[^=]|=\s*.checked.)/i,Se=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function ke(e,t){return S(e,"table")&&S(11!==t.nodeType?t:t.firstChild,"tr")&&E(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Le(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function je(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n<r;n++)E.event.add(t,i,s[i][n]);G.hasData(e)&&(o=G.access(e),a=E.extend({},o),G.set(t,a))}}function qe(n,r,i,o){r=v(r);var e,t,a,s,u,l,c=0,f=n.length,d=f-1,p=r[0],h=b(p);if(h||1<f&&"string"==typeof p&&!m.checkClone&&Ne.test(p))return n.each(function(e){var t=n.eq(e);h&&(r[0]=p.call(this,e,t.html())),qe(t,r,i,o)});if(f&&(t=(e=me(r,n[0].ownerDocument,!1,n,o)).firstChild,1===e.childNodes.length&&(e=t),t||o)){for(s=(a=E.map(ge(e,"script"),De)).length;c<f;c++)u=e,c!==d&&(u=E.clone(u,!0,!0),s&&E.merge(a,ge(u,"script"))),i.call(n[c],u,c);if(s)for(l=a[a.length-1].ownerDocument,E.map(a,Le),c=0;c<s;c++)u=a[c],pe.test(u.type||"")&&!Y.access(u,"globalEval")&&E.contains(l,u)&&(u.src&&"module"!==(u.type||"").toLowerCase()?E._evalUrl&&!u.noModule&&E._evalUrl(u.src,{nonce:u.nonce||u.getAttribute("nonce")},l):C(u.textContent.replace(Se,""),u,l))}return n}function Oe(e,t,n){for(var r,i=t?E.filter(t,e):e,o=0;null!=(r=i[o]);o++)n||1!==r.nodeType||E.cleanData(ge(r)),r.parentNode&&(n&&ie(r)&&ve(ge(r,"script")),r.parentNode.removeChild(r));return e}E.extend({htmlPrefilter:function(e){return e},clone:function(e,t,n){var r,i,o,a,s,u,l,c=e.cloneNode(!0),f=ie(e);if(!(m.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||E.isXMLDoc(e)))for(a=ge(c),r=0,i=(o=ge(e)).length;r<i;r++)s=o[r],u=a[r],void 0,"input"===(l=u.nodeName.toLowerCase())&&fe.test(s.type)?u.checked=s.checked:"input"!==l&&"textarea"!==l||(u.defaultValue=s.defaultValue);if(t)if(n)for(o=o||ge(e),a=a||ge(c),r=0,i=o.length;r<i;r++)je(o[r],a[r]);else je(e,c);return 0<(a=ge(c,"script")).length&&ve(a,!f&&ge(e,"script")),c},cleanData:function(e){for(var t,n,r,i=E.event.special,o=0;void 0!==(n=e[o]);o++)if(X(n)){if(t=n[Y.expando]){if(t.events)for(r in t.events)i[r]?E.event.remove(n,r):E.removeEvent(n,r,t.handle);n[Y.expando]=void 0}n[G.expando]&&(n[G.expando]=void 0)}}}),E.fn.extend({detach:function(e){return Oe(this,e,!0)},remove:function(e){return Oe(this,e)},text:function(e){return $(this,function(e){return void 0===e?E.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return qe(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||ke(this,e).appendChild(e)})},prepend:function(){return qe(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=ke(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return qe(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(E.cleanData(ge(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return E.clone(this,e,t)})},html:function(e){return $(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!he[(de.exec(e)||["",""])[1].toLowerCase()]){e=E.htmlPrefilter(e);try{for(;n<r;n++)1===(t=this[n]||{}).nodeType&&(E.cleanData(ge(t,!1)),t.innerHTML=e);t=0}catch(e){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var n=[];return qe(this,arguments,function(e){var t=this.parentNode;E.inArray(this,n)<0&&(E.cleanData(ge(this)),t&&t.replaceChild(e,this))},n)}}),E.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,a){E.fn[e]=function(e){for(var t,n=[],r=E(e),i=r.length-1,o=0;o<=i;o++)t=o===i?this:this.clone(!0),E(r[o])[a](t),u.apply(n,t.get());return this.pushStack(n)}});var Pe=new RegExp("^("+ee+")(?!px)[a-z%]+$","i"),He=function(e){var t=e.ownerDocument.defaultView;return t&&t.opener||(t=g),t.getComputedStyle(e)},Ie=function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t)e.style[i]=o[i];return r},Re=new RegExp(ne.join("|"),"i");function Be(e,t,n){var r,i,o,a,s=e.style;return(n=n||He(e))&&(""!==(a=n.getPropertyValue(t)||n[t])||ie(e)||(a=E.style(e,t)),!m.pixelBoxStyles()&&Pe.test(a)&&Re.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0!==a?a+"":a}function Me(e,t){return{get:function(){if(!e())return(this.get=t).apply(this,arguments);delete this.get}}}!function(){function e(){if(l){u.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",l.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",re.appendChild(u).appendChild(l);var e=g.getComputedStyle(l);n="1%"!==e.top,s=12===t(e.marginLeft),l.style.right="60%",o=36===t(e.right),r=36===t(e.width),l.style.position="absolute",i=12===t(l.offsetWidth/3),re.removeChild(u),l=null}}function t(e){return Math.round(parseFloat(e))}var n,r,i,o,a,s,u=w.createElement("div"),l=w.createElement("div");l.style&&(l.style.backgroundClip="content-box",l.cloneNode(!0).style.backgroundClip="",m.clearCloneStyle="content-box"===l.style.backgroundClip,E.extend(m,{boxSizingReliable:function(){return e(),r},pixelBoxStyles:function(){return e(),o},pixelPosition:function(){return e(),n},reliableMarginLeft:function(){return e(),s},scrollboxSize:function(){return e(),i},reliableTrDimensions:function(){var e,t,n,r;return null==a&&(e=w.createElement("table"),t=w.createElement("tr"),n=w.createElement("div"),e.style.cssText="position:absolute;left:-11111px;border-collapse:separate",t.style.cssText="border:1px solid",t.style.height="1px",n.style.height="9px",n.style.display="block",re.appendChild(e).appendChild(t).appendChild(n),r=g.getComputedStyle(t),a=parseInt(r.height,10)+parseInt(r.borderTopWidth,10)+parseInt(r.borderBottomWidth,10)===t.offsetHeight,re.removeChild(e)),a}}))}();var We=["Webkit","Moz","ms"],Fe=w.createElement("div").style,$e={};function ze(e){var t=E.cssProps[e]||$e[e];return t||(e in Fe?e:$e[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=We.length;while(n--)if((e=We[n]+t)in Fe)return e}(e)||e)}var _e,Ue,Ve=/^(none|table(?!-c[ea]).+)/,Xe=/^--/,Qe={position:"absolute",visibility:"hidden",display:"block"},Ye={letterSpacing:"0",fontWeight:"400"};function Ge(e,t,n){var r=te.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):t}function Ke(e,t,n,r,i,o){var a="width"===t?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=E.css(e,n+ne[a],!0,i)),r?("content"===n&&(u-=E.css(e,"padding"+ne[a],!0,i)),"margin"!==n&&(u-=E.css(e,"border"+ne[a]+"Width",!0,i))):(u+=E.css(e,"padding"+ne[a],!0,i),"padding"!==n?u+=E.css(e,"border"+ne[a]+"Width",!0,i):s+=E.css(e,"border"+ne[a]+"Width",!0,i));return!r&&0<=o&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u}function Je(e,t,n){var r=He(e),i=(!m.boxSizingReliable()||n)&&"border-box"===E.css(e,"boxSizing",!1,r),o=i,a=Be(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(Pe.test(a)){if(!n)return a;a="auto"}return(!m.boxSizingReliable()&&i||!m.reliableTrDimensions()&&S(e,"tr")||"auto"===a||!parseFloat(a)&&"inline"===E.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===E.css(e,"boxSizing",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Ke(e,t,n||(i?"border":"content"),o,r,a)+"px"}E.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Be(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=V(t),u=Xe.test(t),l=e.style;if(u||(t=ze(s)),a=E.cssHooks[t]||E.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"===(o=typeof n)&&(i=te.exec(n))&&i[1]&&(n=function(e,t,n,r){var i,o,a=20,s=r?function(){return r.cur()}:function(){return E.css(e,t,"")},u=s(),l=n&&n[3]||(E.cssNumber[t]?"":"px"),c=e.nodeType&&(E.cssNumber[t]||"px"!==l&&+u)&&te.exec(E.css(e,t));if(c&&c[3]!==l){u/=2,l=l||c[3],c=+u||1;while(a--)E.style(e,t,c+l),(1-o)*(1-(o=s()/u||.5))<=0&&(a=0),c/=o;c*=2,E.style(e,t,c+l),n=n||[]}return n&&(c=+c||+u||0,i=n[1]?c+(n[1]+1)*n[2]:+n[2],r&&(r.unit=l,r.start=c,r.end=i)),i}(e,t,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(E.cssNumber[s]?"":"px")),m.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=V(t);return Xe.test(t)||(t=ze(s)),(a=E.cssHooks[t]||E.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Be(e,t,r)),"normal"===i&&t in Ye&&(i=Ye[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),E.each(["height","width"],function(e,u){E.cssHooks[u]={get:function(e,t,n){if(t)return!Ve.test(E.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Je(e,u,n):Ie(e,Qe,function(){return Je(e,u,n)})},set:function(e,t,n){var r,i=He(e),o=!m.scrollboxSize()&&"absolute"===i.position,a=(o||n)&&"border-box"===E.css(e,"boxSizing",!1,i),s=n?Ke(e,u,n,a,i):0;return a&&o&&(s-=Math.ceil(e["offset"+u[0].toUpperCase()+u.slice(1)]-parseFloat(i[u])-Ke(e,u,"border",!1,i)-.5)),s&&(r=te.exec(t))&&"px"!==(r[3]||"px")&&(e.style[u]=t,t=E.css(e,u)),Ge(0,t,s)}}}),E.cssHooks.marginLeft=Me(m.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Be(e,"marginLeft"))||e.getBoundingClientRect().left-Ie(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),E.each({margin:"",padding:"",border:"Width"},function(i,o){E.cssHooks[i+o]={expand:function(e){for(var t=0,n={},r="string"==typeof e?e.split(" "):[e];t<4;t++)n[i+ne[t]+o]=r[t]||r[t-2]||r[0];return n}},"margin"!==i&&(E.cssHooks[i+o].set=Ge)}),E.fn.extend({css:function(e,t){return $(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=He(e),i=t.length;a<i;a++)o[t[a]]=E.css(e,t[a],!1,r);return o}return void 0!==n?E.style(e,t,n):E.css(e,t)},e,t,1<arguments.length)}}),E.fn.delay=function(r,e){return r=E.fx&&E.fx.speeds[r]||r,e=e||"fx",this.queue(e,function(e,t){var n=g.setTimeout(e,r);t.stop=function(){g.clearTimeout(n)}})},_e=w.createElement("input"),Ue=w.createElement("select").appendChild(w.createElement("option")),_e.type="checkbox",m.checkOn=""!==_e.value,m.optSelected=Ue.selected,(_e=w.createElement("input")).value="t",_e.type="radio",m.radioValue="t"===_e.value;var Ze,et=E.expr.attrHandle;E.fn.extend({attr:function(e,t){return $(this,E.attr,e,t,1<arguments.length)},removeAttr:function(e){return this.each(function(){E.removeAttr(this,e)})}}),E.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?E.prop(e,t,n):(1===o&&E.isXMLDoc(e)||(i=E.attrHooks[t.toLowerCase()]||(E.expr.match.bool.test(t)?Ze:void 0)),void 0!==n?null===n?void E.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=E.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!m.radioValue&&"radio"===t&&S(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(H);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),Ze={set:function(e,t,n){return!1===t?E.removeAttr(e,n):e.setAttribute(n,n),n}},E.each(E.expr.match.bool.source.match(/\w+/g),function(e,t){var a=et[t]||E.find.attr;et[t]=function(e,t,n){var r,i,o=t.toLowerCase();return n||(i=et[o],et[o]=r,r=null!=a(e,t,n)?o:null,et[o]=i),r}});var tt=/^(?:input|select|textarea|button)$/i,nt=/^(?:a|area)$/i;function rt(e){return(e.match(H)||[]).join(" ")}function it(e){return e.getAttribute&&e.getAttribute("class")||""}function ot(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(H)||[]}E.fn.extend({prop:function(e,t){return $(this,E.prop,e,t,1<arguments.length)},removeProp:function(e){return this.each(function(){delete this[E.propFix[e]||e]})}}),E.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&E.isXMLDoc(e)||(t=E.propFix[t]||t,i=E.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=E.find.attr(e,"tabindex");return t?parseInt(t,10):tt.test(e.nodeName)||nt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),m.optSelected||(E.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),E.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){E.propFix[this.toLowerCase()]=this}),E.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(b(t))return this.each(function(e){E(this).addClass(t.call(this,e,it(this)))});if((e=ot(t)).length)while(n=this[u++])if(i=it(n),r=1===n.nodeType&&" "+rt(i)+" "){a=0;while(o=e[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=rt(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(b(t))return this.each(function(e){E(this).removeClass(t.call(this,e,it(this)))});if(!arguments.length)return this.attr("class","");if((e=ot(t)).length)while(n=this[u++])if(i=it(n),r=1===n.nodeType&&" "+rt(i)+" "){a=0;while(o=e[a++])while(-1<r.indexOf(" "+o+" "))r=r.replace(" "+o+" "," ");i!==(s=rt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(i,t){var o=typeof i,a="string"===o||Array.isArray(i);return"boolean"==typeof t&&a?t?this.addClass(i):this.removeClass(i):b(i)?this.each(function(e){E(this).toggleClass(i.call(this,e,it(this),t),t)}):this.each(function(){var e,t,n,r;if(a){t=0,n=E(this),r=ot(i);while(e=r[t++])n.hasClass(e)?n.removeClass(e):n.addClass(e)}else void 0!==i&&"boolean"!==o||((e=it(this))&&Y.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===i?"":Y.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&-1<(" "+rt(it(n))+" ").indexOf(t))return!0;return!1}});var at=/\r/g;E.fn.extend({val:function(n){var r,e,i,t=this[0];return arguments.length?(i=b(n),this.each(function(e){var t;1===this.nodeType&&(null==(t=i?n.call(this,e,E(this).val()):n)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=E.map(t,function(e){return null==e?"":e+""})),(r=E.valHooks[this.type]||E.valHooks[this.nodeName.toLowerCase()])&&"set"in r&&void 0!==r.set(this,t,"value")||(this.value=t))})):t?(r=E.valHooks[t.type]||E.valHooks[t.nodeName.toLowerCase()])&&"get"in r&&void 0!==(e=r.get(t,"value"))?e:"string"==typeof(e=t.value)?e.replace(at,""):null==e?"":e:void 0}}),E.extend({valHooks:{option:{get:function(e){var t=E.find.attr(e,"value");return null!=t?t:rt(E.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r<u;r++)if(((n=i[r]).selected||r===o)&&!n.disabled&&(!n.parentNode.disabled||!S(n.parentNode,"optgroup"))){if(t=E(n).val(),a)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=E.makeArray(t),a=i.length;while(a--)((r=i[a]).selected=-1<E.inArray(E.valHooks.option.get(r),o))&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),E.each(["radio","checkbox"],function(){E.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=-1<E.inArray(E(e).val(),t)}},m.checkOn||(E.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),m.focusin="onfocusin"in g;var st=/^(?:focusinfocus|focusoutblur)$/,ut=function(e){e.stopPropagation()};E.extend(E.event,{trigger:function(e,t,n,r){var i,o,a,s,u,l,c,f,d=[n||w],p=y.call(e,"type")?e.type:e,h=y.call(e,"namespace")?e.namespace.split("."):[];if(o=f=a=n=n||w,3!==n.nodeType&&8!==n.nodeType&&!st.test(p+E.event.triggered)&&(-1<p.indexOf(".")&&(p=(h=p.split(".")).shift(),h.sort()),u=p.indexOf(":")<0&&"on"+p,(e=e[E.expando]?e:new E.Event(p,"object"==typeof e&&e)).isTrigger=r?2:3,e.namespace=h.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=null==t?[e]:E.makeArray(t,[e]),c=E.event.special[p]||{},r||!c.trigger||!1!==c.trigger.apply(n,t))){if(!r&&!c.noBubble&&!x(n)){for(s=c.delegateType||p,st.test(s+p)||(o=o.parentNode);o;o=o.parentNode)d.push(o),a=o;a===(n.ownerDocument||w)&&d.push(a.defaultView||a.parentWindow||g)}i=0;while((o=d[i++])&&!e.isPropagationStopped())f=o,e.type=1<i?s:c.bindType||p,(l=(Y.get(o,"events")||Object.create(null))[e.type]&&Y.get(o,"handle"))&&l.apply(o,t),(l=u&&o[u])&&l.apply&&X(o)&&(e.result=l.apply(o,t),!1===e.result&&e.preventDefault());return e.type=p,r||e.isDefaultPrevented()||c._default&&!1!==c._default.apply(d.pop(),t)||!X(n)||u&&b(n[p])&&!x(n)&&((a=n[u])&&(n[u]=null),E.event.triggered=p,e.isPropagationStopped()&&f.addEventListener(p,ut),n[p](),e.isPropagationStopped()&&f.removeEventListener(p,ut),E.event.triggered=void 0,a&&(n[u]=a)),e.result}},simulate:function(e,t,n){var r=E.extend(new E.Event,n,{type:e,isSimulated:!0});E.event.trigger(r,null,t)}}),E.fn.extend({trigger:function(e,t){return this.each(function(){E.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return E.event.trigger(e,t,n,!0)}}),m.focusin||E.each({focus:"focusin",blur:"focusout"},function(n,r){var i=function(e){E.event.simulate(r,e.target,E.event.fix(e))};E.event.special[r]={setup:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r);t||e.addEventListener(n,i,!0),Y.access(e,r,(t||0)+1)},teardown:function(){var e=this.ownerDocument||this.document||this,t=Y.access(e,r)-1;t?Y.access(e,r,t):(e.removeEventListener(n,i,!0),Y.remove(e,r))}}}),E.parseXML=function(e){var t,n;if(!e||"string"!=typeof e)return null;try{t=(new g.DOMParser).parseFromString(e,"text/xml")}catch(e){}return n=t&&t.getElementsByTagName("parsererror")[0],t&&!n||E.error("Invalid XML: "+(n?E.map(n.childNodes,function(e){return e.textContent}).join("\n"):e)),t};var lt,ct=/\[\]$/,ft=/\r?\n/g,dt=/^(?:submit|button|image|reset|file)$/i,pt=/^(?:input|select|textarea|keygen)/i;function ht(n,e,r,i){var t;if(Array.isArray(e))E.each(e,function(e,t){r||ct.test(n)?i(n,t):ht(n+"["+("object"==typeof t&&null!=t?e:"")+"]",t,r,i)});else if(r||"object"!==T(e))i(n,e);else for(t in e)ht(n+"["+t+"]",e[t],r,i)}E.param=function(e,t){var n,r=[],i=function(e,t){var n=b(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!E.isPlainObject(e))E.each(e,function(){i(this.name,this.value)});else for(n in e)ht(n,e[n],t,i);return r.join("&")},E.fn.extend({serialize:function(){return E.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=E.prop(this,"elements");return e?E.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!E(this).is(":disabled")&&pt.test(this.nodeName)&&!dt.test(e)&&(this.checked||!fe.test(e))}).map(function(e,t){var n=E(this).val();return null==n?null:Array.isArray(n)?E.map(n,function(e){return{name:t.name,value:e.replace(ft,"\r\n")}}):{name:t.name,value:n.replace(ft,"\r\n")}}).get()}}),E.fn.extend({wrapAll:function(e){var t;return this[0]&&(b(e)&&(e=e.call(this[0])),t=E(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(n){return b(n)?this.each(function(e){E(this).wrapInner(n.call(this,e))}):this.each(function(){var e=E(this),t=e.contents();t.length?t.wrapAll(n):e.append(n)})},wrap:function(t){var n=b(t);return this.each(function(e){E(this).wrapAll(n?t.call(this,e):t)})},unwrap:function(e){return this.parent(e).not("body").each(function(){E(this).replaceWith(this.childNodes)}),this}}),E.expr.pseudos.hidden=function(e){return!E.expr.pseudos.visible(e)},E.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},m.createHTMLDocument=((lt=w.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===lt.childNodes.length),E.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(m.createHTMLDocument?((r=(t=w.implementation.createHTMLDocument("")).createElement("base")).href=w.location.href,t.head.appendChild(r)):t=w),o=!n&&[],(i=k.exec(e))?[t.createElement(i[1])]:(i=me([e],t,o),o&&o.length&&E(o).remove(),E.merge([],i.childNodes)));var r,i,o},E.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=E.css(e,"position"),c=E(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=E.css(e,"top"),u=E.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),b(t)&&(t=t.call(e,n,E.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},E.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){E.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===E.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===E.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=E(e).offset()).top+=E.css(e,"borderTopWidth",!0),i.left+=E.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-E.css(r,"marginTop",!0),left:t.left-i.left-E.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===E.css(e,"position"))e=e.offsetParent;return e||re})}}),E.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;E.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),E.each(["top","left"],function(e,n){E.cssHooks[n]=Me(m.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?E(e).position()[n]+"px":t})}),E.each({Height:"height",Width:"width"},function(a,s){E.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){E.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?E.css(e,t,i):E.style(e,t,n,i)},s,n?e:void 0,n)}})}),E.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),E.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(e,n){E.fn[n]=function(e,t){return 0<arguments.length?this.on(n,null,e,t):this.trigger(n)}});var gt=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;E.proxy=function(e,t){var n,r,i;if("string"==typeof t&&(n=e[t],t=e,e=n),b(e))return r=s.call(arguments,2),(i=function(){return e.apply(t||this,r.concat(s.call(arguments)))}).guid=e.guid=e.guid||E.guid++,i},E.holdReady=function(e){e?E.readyWait++:E.ready(!0)},E.isArray=Array.isArray,E.parseJSON=JSON.parse,E.nodeName=S,E.isFunction=b,E.isWindow=x,E.camelCase=V,E.type=T,E.now=Date.now,E.isNumeric=function(e){var t=E.type(e);return("number"===t||"string"===t)&&!isNaN(e-parseFloat(e))},E.trim=function(e){return null==e?"":(e+"").replace(gt,"")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return E});var vt=g.jQuery,yt=g.$;return E.noConflict=function(e){return g.$===E&&(g.$=yt),e&&g.jQuery===E&&(g.jQuery=vt),E},"undefined"==typeof e&&(g.jQuery=g.$=E),E});
@@ -1,5 +0,0 @@-/*- Copyright (C) Federico Zivolo 2018- Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT).- */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e)return document.body;switch(e.nodeName){case'HTML':case'BODY':return e.ownerDocument.body;case'#document':return e.body;}var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll|overlay)/.test(r+s+p)?e:n(o(e))}function r(e){if(!e)return document.documentElement;for(var o=ie(10)?document.body:null,n=e.offsetParent;n===o&&e.nextElementSibling;)n=(e=e.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(n.nodeName)&&'static'===t(n,'position')?r(n):n:e?e.ownerDocument.documentElement:document.documentElement}function p(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||r(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function d(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,n=o?e:t,i=o?t:e,a=document.createRange();a.setStart(n,0),a.setEnd(i,0);var l=a.commonAncestorContainer;if(e!==l&&t!==l||n.contains(i))return p(l)?l:r(l);var f=s(e);return f.host?d(f.host,t):d(e,s(t).host)}function a(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:'top',o='top'===t?'scrollTop':'scrollLeft',n=e.nodeName;if('BODY'===n||'HTML'===n){var i=e.ownerDocument.documentElement,r=e.ownerDocument.scrollingElement||i;return r[o]}return e[o]}function l(e,t){var o=2<arguments.length&&void 0!==arguments[2]&&arguments[2],n=a(t,'top'),i=a(t,'left'),r=o?-1:1;return e.top+=n*r,e.bottom+=n*r,e.left+=i*r,e.right+=i*r,e}function f(e,t){var o='x'===t?'Left':'Top',n='Left'==o?'Right':'Bottom';return parseFloat(e['border'+o+'Width'],10)+parseFloat(e['border'+n+'Width'],10)}function m(e,t,o,n){return Q(t['offset'+e],t['scroll'+e],o['client'+e],o['offset'+e],o['scroll'+e],ie(10)?o['offset'+e]+n['margin'+('Height'===e?'Top':'Left')]+n['margin'+('Height'===e?'Bottom':'Right')]:0)}function h(){var e=document.body,t=document.documentElement,o=ie(10)&&getComputedStyle(t);return{height:m('Height',e,t,o),width:m('Width',e,t,o)}}function c(e){return de({},e,{right:e.left+e.width,bottom:e.top+e.height})}function g(e){var o={};try{if(ie(10)){o=e.getBoundingClientRect();var n=a(e,'top'),i=a(e,'left');o.top+=n,o.left+=i,o.bottom+=n,o.right+=i}else o=e.getBoundingClientRect()}catch(t){}var r={left:o.left,top:o.top,width:o.right-o.left,height:o.bottom-o.top},p='HTML'===e.nodeName?h():{},s=p.width||e.clientWidth||r.right-r.left,d=p.height||e.clientHeight||r.bottom-r.top,l=e.offsetWidth-s,m=e.offsetHeight-d;if(l||m){var g=t(e);l-=f(g,'x'),m-=f(g,'y'),r.width-=l,r.height-=m}return c(r)}function u(e,o){var i=2<arguments.length&&void 0!==arguments[2]&&arguments[2],r=ie(10),p='HTML'===o.nodeName,s=g(e),d=g(o),a=n(e),f=t(o),m=parseFloat(f.borderTopWidth,10),h=parseFloat(f.borderLeftWidth,10);i&&'HTML'===o.nodeName&&(d.top=Q(d.top,0),d.left=Q(d.left,0));var u=c({top:s.top-d.top-m,left:s.left-d.left-h,width:s.width,height:s.height});if(u.marginTop=0,u.marginLeft=0,!r&&p){var b=parseFloat(f.marginTop,10),y=parseFloat(f.marginLeft,10);u.top-=m-b,u.bottom-=m-b,u.left-=h-y,u.right-=h-y,u.marginTop=b,u.marginLeft=y}return(r&&!i?o.contains(a):o===a&&'BODY'!==a.nodeName)&&(u=l(u,o)),u}function b(e){var t=1<arguments.length&&void 0!==arguments[1]&&arguments[1],o=e.ownerDocument.documentElement,n=u(e,o),i=Q(o.clientWidth,window.innerWidth||0),r=Q(o.clientHeight,window.innerHeight||0),p=t?0:a(o),s=t?0:a(o,'left'),d={top:p-n.top+n.marginTop,left:s-n.left+n.marginLeft,width:i,height:r};return c(d)}function y(e){var n=e.nodeName;return'BODY'===n||'HTML'===n?!1:'fixed'===t(e,'position')||y(o(e))}function w(e){if(!e||!e.parentElement||ie())return document.documentElement;for(var o=e.parentElement;o&&'none'===t(o,'transform');)o=o.parentElement;return o||document.documentElement}function E(e,t,i,r){var p=4<arguments.length&&void 0!==arguments[4]&&arguments[4],s={top:0,left:0},a=p?w(e):d(e,t);if('viewport'===r)s=b(a,p);else{var l;'scrollParent'===r?(l=n(o(t)),'BODY'===l.nodeName&&(l=e.ownerDocument.documentElement)):'window'===r?l=e.ownerDocument.documentElement:l=r;var f=u(l,a,p);if('HTML'===l.nodeName&&!y(a)){var m=h(),c=m.height,g=m.width;s.top+=f.top-f.marginTop,s.bottom=c+f.top,s.left+=f.left-f.marginLeft,s.right=g+f.left}else s=f}return s.left+=i,s.top+=i,s.right-=i,s.bottom-=i,s}function v(e){var t=e.width,o=e.height;return t*o}function x(e,t,o,n,i){var r=5<arguments.length&&void 0!==arguments[5]?arguments[5]:0;if(-1===e.indexOf('auto'))return e;var p=E(o,n,r,i),s={top:{width:p.width,height:t.top-p.top},right:{width:p.right-t.right,height:p.height},bottom:{width:p.width,height:p.bottom-t.bottom},left:{width:t.left-p.left,height:p.height}},d=Object.keys(s).map(function(e){return de({key:e},s[e],{area:v(s[e])})}).sort(function(e,t){return t.area-e.area}),a=d.filter(function(e){var t=e.width,n=e.height;return t>=o.clientWidth&&n>=o.clientHeight}),l=0<a.length?a[0].key:d[0].key,f=e.split('-')[1];return l+(f?'-'+f:'')}function O(e,t,o){var n=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null,i=n?w(t):d(t,o);return u(o,i,n)}function L(e){var t=getComputedStyle(e),o=parseFloat(t.marginTop)+parseFloat(t.marginBottom),n=parseFloat(t.marginLeft)+parseFloat(t.marginRight),i={width:e.offsetWidth+n,height:e.offsetHeight+o};return i}function S(e){var t={left:'right',right:'left',bottom:'top',top:'bottom'};return e.replace(/left|right|bottom|top/g,function(e){return t[e]})}function T(e,t,o){o=o.split('-')[0];var n=L(e),i={width:n.width,height:n.height},r=-1!==['right','left'].indexOf(o),p=r?'top':'left',s=r?'left':'top',d=r?'height':'width',a=r?'width':'height';return i[p]=t[p]+t[d]/2-n[d]/2,i[s]=o===s?t[s]-n[a]:t[S(s)],i}function D(e,t){return Array.prototype.find?e.find(t):e.filter(t)[0]}function C(e,t,o){if(Array.prototype.findIndex)return e.findIndex(function(e){return e[t]===o});var n=D(e,function(e){return e[t]===o});return e.indexOf(n)}function N(t,o,n){var i=void 0===n?t:t.slice(0,C(t,'name',n));return i.forEach(function(t){t['function']&&console.warn('`modifier.function` is deprecated, use `modifier.fn`!');var n=t['function']||t.fn;t.enabled&&e(n)&&(o.offsets.popper=c(o.offsets.popper),o.offsets.reference=c(o.offsets.reference),o=n(o,t))}),o}function k(){if(!this.state.isDestroyed){var e={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};e.offsets.reference=O(this.state,this.popper,this.reference,this.options.positionFixed),e.placement=x(this.options.placement,e.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),e.originalPlacement=e.placement,e.positionFixed=this.options.positionFixed,e.offsets.popper=T(this.popper,e.offsets.reference,e.placement),e.offsets.popper.position=this.options.positionFixed?'fixed':'absolute',e=N(this.modifiers,e),this.state.isCreated?this.options.onUpdate(e):(this.state.isCreated=!0,this.options.onCreate(e))}}function P(e,t){return e.some(function(e){var o=e.name,n=e.enabled;return n&&o===t})}function W(e){for(var t=[!1,'ms','Webkit','Moz','O'],o=e.charAt(0).toUpperCase()+e.slice(1),n=0;n<t.length;n++){var i=t[n],r=i?''+i+o:e;if('undefined'!=typeof document.body.style[r])return r}return null}function B(){return this.state.isDestroyed=!0,P(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[W('transform')]=''),this.disableEventListeners(),this.options.removeOnDestroy&&this.popper.parentNode.removeChild(this.popper),this}function H(e){var t=e.ownerDocument;return t?t.defaultView:window}function A(e,t,o,i){var r='BODY'===e.nodeName,p=r?e.ownerDocument.defaultView:e;p.addEventListener(t,o,{passive:!0}),r||A(n(p.parentNode),t,o,i),i.push(p)}function I(e,t,o,i){o.updateBound=i,H(e).addEventListener('resize',o.updateBound,{passive:!0});var r=n(e);return A(r,'scroll',o.updateBound,o.scrollParents),o.scrollElement=r,o.eventsEnabled=!0,o}function M(){this.state.eventsEnabled||(this.state=I(this.reference,this.options,this.state,this.scheduleUpdate))}function F(e,t){return H(e).removeEventListener('resize',t.updateBound),t.scrollParents.forEach(function(e){e.removeEventListener('scroll',t.updateBound)}),t.updateBound=null,t.scrollParents=[],t.scrollElement=null,t.eventsEnabled=!1,t}function R(){this.state.eventsEnabled&&(cancelAnimationFrame(this.scheduleUpdate),this.state=F(this.reference,this.state))}function U(e){return''!==e&&!isNaN(parseFloat(e))&&isFinite(e)}function Y(e,t){Object.keys(t).forEach(function(o){var n='';-1!==['width','height','top','right','bottom','left'].indexOf(o)&&U(t[o])&&(n='px'),e.style[o]=t[o]+n})}function j(e,t){Object.keys(t).forEach(function(o){var n=t[o];!1===n?e.removeAttribute(o):e.setAttribute(o,t[o])})}function q(e,t,o){var n=D(e,function(e){var o=e.name;return o===t}),i=!!n&&e.some(function(e){return e.name===o&&e.enabled&&e.order<n.order});if(!i){var r='`'+t+'`';console.warn('`'+o+'`'+' modifier is required by '+r+' modifier in order to work, be sure to include it before '+r+'!')}return i}function K(e){return'end'===e?'start':'start'===e?'end':e}function V(e){var t=1<arguments.length&&void 0!==arguments[1]&&arguments[1],o=le.indexOf(e),n=le.slice(o+1).concat(le.slice(0,o));return t?n.reverse():n}function z(e,t,o,n){var i=e.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),r=+i[1],p=i[2];if(!r)return e;if(0===p.indexOf('%')){var s;switch(p){case'%p':s=o;break;case'%':case'%r':default:s=n;}var d=c(s);return d[t]/100*r}if('vh'===p||'vw'===p){var a;return a='vh'===p?Q(document.documentElement.clientHeight,window.innerHeight||0):Q(document.documentElement.clientWidth,window.innerWidth||0),a/100*r}return r}function G(e,t,o,n){var i=[0,0],r=-1!==['right','left'].indexOf(n),p=e.split(/(\+|\-)/).map(function(e){return e.trim()}),s=p.indexOf(D(p,function(e){return-1!==e.search(/,|\s/)}));p[s]&&-1===p[s].indexOf(',')&&console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');var d=/\s*,\s*|\s+/,a=-1===s?[p]:[p.slice(0,s).concat([p[s].split(d)[0]]),[p[s].split(d)[1]].concat(p.slice(s+1))];return a=a.map(function(e,n){var i=(1===n?!r:r)?'height':'width',p=!1;return e.reduce(function(e,t){return''===e[e.length-1]&&-1!==['+','-'].indexOf(t)?(e[e.length-1]=t,p=!0,e):p?(e[e.length-1]+=t,p=!1,e):e.concat(t)},[]).map(function(e){return z(e,i,t,o)})}),a.forEach(function(e,t){e.forEach(function(o,n){U(o)&&(i[t]+=o*('-'===e[n-1]?-1:1))})}),i}function _(e,t){var o,n=t.offset,i=e.placement,r=e.offsets,p=r.popper,s=r.reference,d=i.split('-')[0];return o=U(+n)?[+n,0]:G(n,p,s,d),'left'===d?(p.top+=o[0],p.left-=o[1]):'right'===d?(p.top+=o[0],p.left+=o[1]):'top'===d?(p.left+=o[0],p.top-=o[1]):'bottom'===d&&(p.left+=o[0],p.top+=o[1]),e.popper=p,e}for(var X=Math.min,J=Math.floor,Q=Math.max,Z='undefined'!=typeof window&&'undefined'!=typeof document,$=['Edge','Trident','Firefox'],ee=0,te=0;te<$.length;te+=1)if(Z&&0<=navigator.userAgent.indexOf($[te])){ee=1;break}var i=Z&&window.Promise,oe=i?function(e){var t=!1;return function(){t||(t=!0,window.Promise.resolve().then(function(){t=!1,e()}))}}:function(e){var t=!1;return function(){t||(t=!0,setTimeout(function(){t=!1,e()},ee))}},ne={},ie=function(){var e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:'all';return(e=e.toString(),ne.hasOwnProperty(e))?ne[e]:('11'===e?ne[e]=-1!==navigator.userAgent.indexOf('Trident'):'10'===e?ne[e]=-1!==navigator.appVersion.indexOf('MSIE 10'):'all'===e?ne[e]=-1!==navigator.userAgent.indexOf('Trident')||-1!==navigator.userAgent.indexOf('MSIE'):void 0,ne.all=ne.all||Object.keys(ne).some(function(e){return ne[e]}),ne[e])},re=function(e,t){if(!(e instanceof t))throw new TypeError('Cannot call a class as a function')},pe=function(){function e(e,t){for(var o,n=0;n<t.length;n++)o=t[n],o.enumerable=o.enumerable||!1,o.configurable=!0,'value'in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}return function(t,o,n){return o&&e(t.prototype,o),n&&e(t,n),t}}(),se=function(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e},de=Object.assign||function(e){for(var t,o=1;o<arguments.length;o++)for(var n in t=arguments[o],t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e},ae=['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'],le=ae.slice(3),fe={FLIP:'flip',CLOCKWISE:'clockwise',COUNTERCLOCKWISE:'counterclockwise'},me=function(){function t(o,n){var i=this,r=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};re(this,t),this.scheduleUpdate=function(){return requestAnimationFrame(i.update)},this.update=oe(this.update.bind(this)),this.options=de({},t.Defaults,r),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=o&&o.jquery?o[0]:o,this.popper=n&&n.jquery?n[0]:n,this.options.modifiers={},Object.keys(de({},t.Defaults.modifiers,r.modifiers)).forEach(function(e){i.options.modifiers[e]=de({},t.Defaults.modifiers[e]||{},r.modifiers?r.modifiers[e]:{})}),this.modifiers=Object.keys(this.options.modifiers).map(function(e){return de({name:e},i.options.modifiers[e])}).sort(function(e,t){return e.order-t.order}),this.modifiers.forEach(function(t){t.enabled&&e(t.onLoad)&&t.onLoad(i.reference,i.popper,i.options,t,i.state)}),this.update();var p=this.options.eventsEnabled;p&&this.enableEventListeners(),this.state.eventsEnabled=p}return pe(t,[{key:'update',value:function(){return k.call(this)}},{key:'destroy',value:function(){return B.call(this)}},{key:'enableEventListeners',value:function(){return M.call(this)}},{key:'disableEventListeners',value:function(){return R.call(this)}}]),t}();return me.Utils=('undefined'==typeof window?global:window).PopperUtils,me.placements=ae,me.Defaults={placement:'bottom',positionFixed:!1,eventsEnabled:!0,removeOnDestroy:!1,onCreate:function(){},onUpdate:function(){},modifiers:{shift:{order:100,enabled:!0,fn:function(e){var t=e.placement,o=t.split('-')[0],n=t.split('-')[1];if(n){var i=e.offsets,r=i.reference,p=i.popper,s=-1!==['bottom','top'].indexOf(o),d=s?'left':'top',a=s?'width':'height',l={start:se({},d,r[d]),end:se({},d,r[d]+r[a]-p[a])};e.offsets.popper=de({},p,l[n])}return e}},offset:{order:200,enabled:!0,fn:_,offset:0},preventOverflow:{order:300,enabled:!0,fn:function(e,t){var o=t.boundariesElement||r(e.instance.popper);e.instance.reference===o&&(o=r(o));var n=E(e.instance.popper,e.instance.reference,t.padding,o,e.positionFixed);t.boundaries=n;var i=t.priority,p=e.offsets.popper,s={primary:function(e){var o=p[e];return p[e]<n[e]&&!t.escapeWithReference&&(o=Q(p[e],n[e])),se({},e,o)},secondary:function(e){var o='right'===e?'left':'top',i=p[o];return p[e]>n[e]&&!t.escapeWithReference&&(i=X(p[o],n[e]-('right'===e?p.width:p.height))),se({},o,i)}};return i.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';p=de({},p,s[t](e))}),e.offsets.popper=p,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,n=t.reference,i=e.placement.split('-')[0],r=J,p=-1!==['top','bottom'].indexOf(i),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]<r(n[d])&&(e.offsets.popper[d]=r(n[d])-o[a]),o[d]>r(n[s])&&(e.offsets.popper[d]=r(n[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,o){var n;if(!q(e.instance.modifiers,'arrow','keepTogether'))return e;var i=o.element;if('string'==typeof i){if(i=e.instance.popper.querySelector(i),!i)return e;}else if(!e.instance.popper.contains(i))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var r=e.placement.split('-')[0],p=e.offsets,s=p.popper,d=p.reference,a=-1!==['left','right'].indexOf(r),l=a?'height':'width',f=a?'Top':'Left',m=f.toLowerCase(),h=a?'left':'top',g=a?'bottom':'right',u=L(i)[l];d[g]-u<s[m]&&(e.offsets.popper[m]-=s[m]-(d[g]-u)),d[m]+u>s[g]&&(e.offsets.popper[m]+=d[m]+u-s[g]),e.offsets.popper=c(e.offsets.popper);var b=d[m]+d[l]/2-u/2,y=t(e.instance.popper),w=parseFloat(y['margin'+f],10),E=parseFloat(y['border'+f+'Width'],10),v=b-e.offsets.popper[m]-w-E;return v=Q(X(s[l]-u,v),0),e.arrowElement=i,e.offsets.arrow=(n={},se(n,m,Math.round(v)),se(n,h,''),n),e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(P(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=E(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),n=e.placement.split('-')[0],i=S(n),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case fe.FLIP:p=[n,i];break;case fe.CLOCKWISE:p=V(n);break;case fe.COUNTERCLOCKWISE:p=V(n,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(n!==s||p.length===d+1)return e;n=e.placement.split('-')[0],i=S(n);var a=e.offsets.popper,l=e.offsets.reference,f=J,m='left'===n&&f(a.right)>f(l.left)||'right'===n&&f(a.left)<f(l.right)||'top'===n&&f(a.bottom)>f(l.top)||'bottom'===n&&f(a.top)<f(l.bottom),h=f(a.left)<f(o.left),c=f(a.right)>f(o.right),g=f(a.top)<f(o.top),u=f(a.bottom)>f(o.bottom),b='left'===n&&h||'right'===n&&c||'top'===n&&g||'bottom'===n&&u,y=-1!==['top','bottom'].indexOf(n),w=!!t.flipVariations&&(y&&'start'===r&&h||y&&'end'===r&&c||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(n=p[d+1]),w&&(r=K(r)),e.placement=n+(r?'-'+r:''),e.offsets.popper=de({},e.offsets.popper,T(e.instance.popper,e.offsets.reference,e.placement)),e=N(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],n=e.offsets,i=n.popper,r=n.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return i[p?'left':'top']=r[o]-(s?i[p?'width':'height']:0),e.placement=S(t),e.offsets.popper=c(i),e}},hide:{order:800,enabled:!0,fn:function(e){if(!q(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=D(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottom<o.top||t.left>o.right||t.top>o.bottom||t.right<o.left){if(!0===e.hide)return e;e.hide=!0,e.attributes['x-out-of-boundaries']=''}else{if(!1===e.hide)return e;e.hide=!1,e.attributes['x-out-of-boundaries']=!1}return e}},computeStyle:{order:850,enabled:!0,fn:function(e,t){var o=t.x,n=t.y,i=e.offsets.popper,p=D(e.instance.modifiers,function(e){return'applyStyle'===e.name}).gpuAcceleration;void 0!==p&&console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');var s,d,a=void 0===p?t.gpuAcceleration:p,l=r(e.instance.popper),f=g(l),m={position:i.position},h={left:J(i.left),top:J(i.top),bottom:J(i.bottom),right:J(i.right)},c='bottom'===o?'top':'bottom',u='right'===n?'left':'right',b=W('transform');if(d='bottom'==c?-f.height+h.bottom:h.top,s='right'==u?-f.width+h.right:h.left,a&&b)m[b]='translate3d('+s+'px, '+d+'px, 0)',m[c]=0,m[u]=0,m.willChange='transform';else{var y='bottom'==c?-1:1,w='right'==u?-1:1;m[c]=d*y,m[u]=s*w,m.willChange=c+', '+u}var E={"x-placement":e.placement};return e.attributes=de({},E,e.attributes),e.styles=de({},m,e.styles),e.arrowStyles=de({},e.offsets.arrow,e.arrowStyles),e},gpuAcceleration:!0,x:'bottom',y:'right'},applyStyle:{order:900,enabled:!0,fn:function(e){return Y(e.instance.popper,e.styles),j(e.instance.popper,e.attributes),e.arrowElement&&Object.keys(e.arrowStyles).length&&Y(e.arrowElement,e.arrowStyles),e},onLoad:function(e,t,o,n,i){var r=O(i,t,e,o.positionFixed),p=x(o.placement,r,t,e,o.modifiers.flip.boundariesElement,o.modifiers.flip.padding);return t.setAttribute('x-placement',p),Y(t,{position:o.positionFixed?'fixed':'absolute'}),o},gpuAcceleration:void 0}}},me});-//# sourceMappingURL=popper.min.js.map
@@ -1,1 +0,0 @@-{"version":3,"file":"tooltip.min.js","sources":["../../../popper/src/utils/isFunction.js","../../src/index.js"],"sourcesContent":["/**\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 */\nexport default function isFunction(functionToCheck) {\n const getType = {};\n return (\n functionToCheck &&\n getType.toString.call(functionToCheck) === '[object Function]'\n );\n}\n","import Popper from 'popper.js';\nimport isFunction from '../../popper/src/utils/isFunction';\n\nconst DEFAULT_OPTIONS = {\n container: false,\n delay: 0,\n html: false,\n placement: 'top',\n title: '',\n template:\n '<div class=\"tooltip\" role=\"tooltip\"><div class=\"tooltip-arrow\"></div><div class=\"tooltip-inner\"></div></div>',\n trigger: 'hover focus',\n offset: 0,\n arrowSelector: '.tooltip-arrow, .tooltip__arrow',\n innerSelector: '.tooltip-inner, .tooltip__inner',\n};\n\nexport default class Tooltip {\n /**\n * Create a new Tooltip.js instance\n * @class Tooltip\n * @param {HTMLElement} reference - The DOM node used as reference of the tooltip (it can be a jQuery element).\n * @param {Object} options\n * @param {String} options.placement='top'\n * Placement of the popper accepted values: `top(-start, -end), right(-start, -end), bottom(-start, -end),\n * left(-start, -end)`\n * @param {String} options.arrowSelector='.tooltip-arrow, .tooltip__arrow' - className used to locate the DOM arrow element in the tooltip.\n * @param {String} options.innerSelector='.tooltip-inner, .tooltip__inner' - className used to locate the DOM inner element in the tooltip.\n * @param {HTMLElement|String|false} options.container=false - Append the tooltip to a specific element.\n * @param {Number|Object} options.delay=0\n * Delay showing and hiding the tooltip (ms) - does not apply to manual trigger type.\n * If a number is supplied, delay is applied to both hide/show.\n * Object structure is: `{ show: 500, hide: 100 }`\n * @param {Boolean} options.html=false - Insert HTML into the tooltip. If false, the content will inserted with `textContent`.\n * @param {String} [options.template='<div class=\"tooltip\" role=\"tooltip\"><div class=\"tooltip-arrow\"></div><div class=\"tooltip-inner\"></div></div>']\n * Base HTML to used when creating the tooltip.\n * The tooltip's `title` will be injected into the `.tooltip-inner` or `.tooltip__inner`.\n * `.tooltip-arrow` or `.tooltip__arrow` will become the tooltip's arrow.\n * The outermost wrapper element should have the `.tooltip` class.\n * @param {String|HTMLElement|TitleFunction} options.title='' - Default title value if `title` attribute isn't present.\n * @param {String} [options.trigger='hover focus']\n * How tooltip is triggered - click, hover, focus, manual.\n * You may pass multiple triggers; separate them with a space. `manual` cannot be combined with any other trigger.\n * @param {Boolean} options.closeOnClickOutside=false - Close a popper on click outside of the popper and reference element. This has effect only when options.trigger is 'click'.\n * @param {String|HTMLElement} options.boundariesElement\n * The element used as boundaries for the tooltip. For more information refer to Popper.js'\n * [boundariesElement docs](https://popper.js.org/popper-documentation.html)\n * @param {Number|String} options.offset=0 - Offset of the tooltip relative to its reference. For more information refer to Popper.js'\n * [offset docs](https://popper.js.org/popper-documentation.html)\n * @param {Object} options.popperOptions={} - Popper options, will be passed directly to popper instance. For more information refer to Popper.js'\n * [options docs](https://popper.js.org/popper-documentation.html)\n * @return {Object} instance - The generated tooltip instance\n */\n constructor(reference, options) {\n // apply user options over default ones\n options = { ...DEFAULT_OPTIONS, ...options };\n\n reference.jquery && (reference = reference[0]);\n\n // cache reference and options\n this.reference = reference;\n this.options = options;\n\n // get events list\n const events =\n typeof options.trigger === 'string'\n ? options.trigger\n .split(' ')\n .filter(\n trigger => ['click', 'hover', 'focus'].indexOf(trigger) !== -1\n )\n : [];\n\n // set initial state\n this._isOpen = false;\n this._popperOptions = {};\n\n // set event listeners\n this._setEventListeners(reference, events, options);\n }\n\n //\n // Public methods\n //\n\n /**\n * Reveals an element's tooltip. This is considered a \"manual\" triggering of the tooltip.\n * Tooltips with zero-length titles are never displayed.\n * @method Tooltip#show\n * @memberof Tooltip\n */\n show = () => this._show(this.reference, this.options);\n\n /**\n * Hides an element’s tooltip. This is considered a “manual” triggering of the tooltip.\n * @method Tooltip#hide\n * @memberof Tooltip\n */\n hide = () => this._hide();\n\n /**\n * Hides and destroys an element’s tooltip.\n * @method Tooltip#dispose\n * @memberof Tooltip\n */\n dispose = () => this._dispose();\n\n /**\n * Toggles an element’s tooltip. This is considered a “manual” triggering of the tooltip.\n * @method Tooltip#toggle\n * @memberof Tooltip\n */\n toggle = () => {\n if (this._isOpen) {\n return this.hide();\n } else {\n return this.show();\n }\n };\n\n /**\n * Updates the tooltip's title content\n * @method Tooltip#updateTitleContent\n * @memberof Tooltip\n * @param {String|HTMLElement} title - The new content to use for the title\n */\n updateTitleContent = (title) => this._updateTitleContent(title);\n\n //\n // Private methods\n //\n\n _events = [];\n\n /**\n * Creates a new tooltip node\n * @memberof Tooltip\n * @private\n * @param {HTMLElement} reference\n * @param {String} template\n * @param {String|HTMLElement|TitleFunction} title\n * @param {Boolean} allowHtml\n * @return {HTMLElement} tooltipNode\n */\n _create(reference, template, title, allowHtml) {\n // create tooltip element\n const tooltipGenerator = window.document.createElement('div');\n tooltipGenerator.innerHTML = template.trim();\n const tooltipNode = tooltipGenerator.childNodes[0];\n\n // add unique ID to our tooltip (needed for accessibility reasons)\n tooltipNode.id = `tooltip_${Math.random()\n .toString(36)\n .substr(2, 10)}`;\n\n // set initial `aria-hidden` state to `false` (it's visible!)\n tooltipNode.setAttribute('aria-hidden', 'false');\n\n // add title to tooltip\n const titleNode = tooltipGenerator.querySelector(this.options.innerSelector);\n this._addTitleContent(reference, title, allowHtml, titleNode);\n\n // return the generated tooltip node\n return tooltipNode;\n }\n\n _addTitleContent(reference, title, allowHtml, titleNode) {\n if (title.nodeType === 1 || title.nodeType === 11) {\n // if title is a element node or document fragment, append it only if allowHtml is true\n allowHtml && titleNode.appendChild(title);\n } else if (isFunction(title)) {\n // if title is a function, call it and set textContent or innerHtml depending by `allowHtml` value\n const titleText = title.call(reference);\n allowHtml\n ? (titleNode.innerHTML = titleText)\n : (titleNode.textContent = titleText);\n } else {\n // if it's just a simple text, set textContent or innerHtml depending by `allowHtml` value\n allowHtml ? (titleNode.innerHTML = title) : (titleNode.textContent = title);\n }\n }\n\n _show(reference, options) {\n // don't show if it's already visible\n // or if it's not being showed\n if (this._isOpen && !this._isOpening) {\n return this;\n }\n this._isOpen = true;\n\n // if the tooltipNode already exists, just show it\n if (this._tooltipNode) {\n this._tooltipNode.style.visibility = 'visible';\n this._tooltipNode.setAttribute('aria-hidden', 'false');\n this.popperInstance.update();\n return this;\n }\n\n // get title\n const title = reference.getAttribute('title') || options.title;\n\n // don't show tooltip if no title is defined\n if (!title) {\n return this;\n }\n\n // create tooltip node\n const tooltipNode = this._create(\n reference,\n options.template,\n title,\n options.html\n );\n\n // Add `aria-describedby` to our reference element for accessibility reasons\n reference.setAttribute('aria-describedby', tooltipNode.id);\n\n // append tooltip to container\n const container = this._findContainer(options.container, reference);\n\n this._append(tooltipNode, container);\n\n this._popperOptions = {\n ...options.popperOptions,\n placement: options.placement,\n };\n\n this._popperOptions.modifiers = {\n ...this._popperOptions.modifiers,\n arrow: {\n element: this.options.arrowSelector,\n },\n offset: {\n offset: options.offset,\n },\n };\n\n if (options.boundariesElement) {\n this._popperOptions.modifiers.preventOverflow = {\n boundariesElement: options.boundariesElement,\n };\n }\n\n this.popperInstance = new Popper(\n reference,\n tooltipNode,\n this._popperOptions\n );\n\n this._tooltipNode = tooltipNode;\n\n return this;\n }\n\n _hide(/*reference, options*/) {\n // don't hide if it's already hidden\n if (!this._isOpen) {\n return this;\n }\n\n this._isOpen = false;\n\n // hide tooltipNode\n this._tooltipNode.style.visibility = 'hidden';\n this._tooltipNode.setAttribute('aria-hidden', 'true');\n\n return this;\n }\n\n _dispose() {\n // remove event listeners first to prevent any unexpected behaviour\n this._events.forEach(({ func, event }) => {\n this.reference.removeEventListener(event, func);\n });\n this._events = [];\n\n if (this._tooltipNode) {\n this._hide();\n\n // destroy instance\n this.popperInstance.destroy();\n\n // destroy tooltipNode if removeOnDestroy is not set, as popperInstance.destroy() already removes the element\n if (!this.popperInstance.options.removeOnDestroy) {\n this._tooltipNode.parentNode.removeChild(this._tooltipNode);\n this._tooltipNode = null;\n }\n }\n return this;\n }\n\n _findContainer(container, reference) {\n // if container is a query, get the relative element\n if (typeof container === 'string') {\n container = window.document.querySelector(container);\n } else if (container === false) {\n // if container is `false`, set it to reference parent\n container = reference.parentNode;\n }\n return container;\n }\n\n /**\n * Append tooltip to container\n * @memberof Tooltip\n * @private\n * @param {HTMLElement} tooltipNode\n * @param {HTMLElement|String|false} container\n */\n _append(tooltipNode, container) {\n container.appendChild(tooltipNode);\n }\n\n _setEventListeners(reference, events, options) {\n const directEvents = [];\n const oppositeEvents = [];\n\n events.forEach(event => {\n switch (event) {\n case 'hover':\n directEvents.push('mouseenter');\n oppositeEvents.push('mouseleave');\n break;\n case 'focus':\n directEvents.push('focus');\n oppositeEvents.push('blur');\n break;\n case 'click':\n directEvents.push('click');\n oppositeEvents.push('click');\n break;\n }\n });\n\n // schedule show tooltip\n directEvents.forEach(event => {\n const func = evt => {\n if (this._isOpening === true) {\n return;\n }\n evt.usedByTooltip = true;\n this._scheduleShow(reference, options.delay, options, evt);\n };\n this._events.push({ event, func });\n reference.addEventListener(event, func);\n });\n\n // schedule hide tooltip\n oppositeEvents.forEach(event => {\n const func = evt => {\n if (evt.usedByTooltip === true) {\n return;\n }\n this._scheduleHide(reference, options.delay, options, evt);\n };\n this._events.push({ event, func });\n reference.addEventListener(event, func);\n if (event === 'click' && options.closeOnClickOutside) {\n document.addEventListener('mousedown', e => {\n if (!this._isOpening) {\n return;\n }\n const popper = this.popperInstance.popper;\n if (reference.contains(e.target) ||\n popper.contains(e.target)) {\n return;\n }\n func(e);\n }, true);\n }\n });\n }\n\n _scheduleShow(reference, delay, options /*, evt */) {\n this._isOpening = true;\n // defaults to 0\n const computedDelay = (delay && delay.show) || delay || 0;\n this._showTimeout = window.setTimeout(\n () => this._show(reference, options),\n computedDelay\n );\n }\n\n _scheduleHide(reference, delay, options, evt) {\n this._isOpening = false;\n // defaults to 0\n const computedDelay = (delay && delay.hide) || delay || 0;\n window.setTimeout(() => {\n window.clearTimeout(this._showTimeout);\n if (this._isOpen === false) {\n return;\n }\n if (!document.body.contains(this._tooltipNode)) {\n return;\n }\n\n // if we are hiding because of a mouseleave, we must check that the new\n // reference isn't the tooltip, because in this case we don't want to hide it\n if (evt.type === 'mouseleave') {\n const isSet = this._setTooltipNodeEvent(evt, reference, delay, options);\n\n // if we set the new event, don't hide the tooltip yet\n // the new event will take care to hide it if necessary\n if (isSet) {\n return;\n }\n }\n\n this._hide(reference, options);\n }, computedDelay);\n }\n\n _setTooltipNodeEvent = (evt, reference, delay, options) => {\n const relatedreference =\n evt.relatedreference || evt.toElement || evt.relatedTarget;\n\n const callback = evt2 => {\n const relatedreference2 =\n evt2.relatedreference || evt2.toElement || evt2.relatedTarget;\n\n // Remove event listener after call\n this._tooltipNode.removeEventListener(evt.type, callback);\n\n // If the new reference is not the reference element\n if (!reference.contains(relatedreference2)) {\n // Schedule to hide tooltip\n this._scheduleHide(reference, options.delay, options, evt2);\n }\n };\n\n if (this._tooltipNode.contains(relatedreference)) {\n // listen to mouseleave on the tooltip element to be able to hide the tooltip\n this._tooltipNode.addEventListener(evt.type, callback);\n return true;\n }\n\n return false;\n };\n\n _updateTitleContent(title) {\n if(typeof this._tooltipNode === 'undefined') {\n if(typeof this.options.title !== 'undefined') {\n this.options.title = title;\n }\n return;\n }\n const titleNode = this._tooltipNode.parentNode.querySelector(this.options.innerSelector);\n this._clearTitleContent(titleNode, this.options.html, this.reference.getAttribute('title') || this.options.title)\n this._addTitleContent(this.reference, title, this.options.html, titleNode);\n this.options.title = title;\n this.popperInstance.update();\n }\n\n _clearTitleContent(titleNode, allowHtml, lastTitle) {\n if(lastTitle.nodeType === 1 || lastTitle.nodeType === 11) {\n allowHtml && titleNode.removeChild(lastTitle);\n } else {\n allowHtml ? titleNode.innerHTML = '' : titleNode.textContent = '';\n }\n }\n\n}\n\n/**\n * Title function, its context is the Tooltip instance.\n * @memberof Tooltip\n * @callback TitleFunction\n * @return {String} placement - The desired title.\n */\n"],"names":["functionToCheck","getType","toString","call","DEFAULT_OPTIONS","Tooltip","jquery","reference","options","events","trigger","split","filter","indexOf","_isOpen","_popperOptions","_setEventListeners","tooltipGenerator","window","document","createElement","innerHTML","template","trim","tooltipNode","childNodes","id","Math","random","substr","setAttribute","titleNode","querySelector","innerSelector","_addTitleContent","title","nodeType","appendChild","isFunction","titleText","textContent","_isOpening","_tooltipNode","style","visibility","popperInstance","update","getAttribute","_create","html","container","_findContainer","_append","popperOptions","placement","modifiers","arrowSelector","offset","boundariesElement","preventOverflow","_events","forEach","func","event","removeEventListener","_hide","destroy","removeOnDestroy","parentNode","removeChild","directEvents","oppositeEvents","push","usedByTooltip","_scheduleShow","delay","addEventListener","evt","_scheduleHide","closeOnClickOutside","popper","contains","e","target","computedDelay","show","_showTimeout","setTimeout","_show","hide","clearTimeout","body","type","isSet","_setTooltipNodeEvent","_clearTitleContent","lastTitle","dispose","_dispose","toggle","updateTitleContent","_updateTitleContent","relatedreference","toElement","relatedTarget","relatedreference2","evt2"],"mappings":"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"}
@@ -1,1 +0,0 @@-.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}
@@ -1,2 +0,0 @@-/*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */-!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,b;function w(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&b.test(e[s])&&(e[s]=e[s].replace(b,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u<e.length;u++)if("."===(p=e[u]))e.splice(u,1),u-=1;else if(".."===p){if(0===u||1===u&&".."===e[2]||".."===e[u-1])continue;0<u&&(e.splice(u-1,2),u-=2)}e=e.join("/")}if((h||g)&&f){for(u=(n=e.split("/")).length;0<u;u-=1){if(r=n.slice(0,u).join("/"),h)for(d=h.length;0<d;d-=1)if(i=(i=f[h.slice(0,d).join("/")])&&i[r]){o=i,a=u;break}if(o)break;!l&&g&&g[r]&&(l=g[r],c=u)}!o&&l&&(o=l,a=c),o&&(n.splice(0,a,o),e=n.join("/"))}return e}function A(t,n){return function(){var e=a.call(arguments,0);return"string"!=typeof e[0]&&1===e.length&&e.push(null),s.apply(h,e.concat([t,n]))}}function x(t){return function(e){m[t]=e}}function D(e){if(w(v,e)){var t=v[e];delete v[e],_[e]=!0,o.apply(h,t)}if(!w(m,e)&&!w(_,e))throw new Error("No "+e);return m[e]}function c(e){var t,n=e?e.indexOf("!"):-1;return-1<n&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function S(e){return e?c(e):[]}return e&&e.requirejs||(e?n=e:e={},m={},v={},y={},_={},i=Object.prototype.hasOwnProperty,a=[].slice,b=/\.js$/,f=function(e,t){var n,r=c(e),i=r[0],o=t[1];return e=r[1],i&&(n=D(i=l(i,o))),i?e=n&&n.normalize?n.normalize(e,function(t){return function(e){return l(e,t)}}(o)):l(e,o):(i=(r=c(e=l(e,o)))[0],e=r[1],i&&(n=D(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},g={require:function(e){return A(e)},exports:function(e){var t=m[e];return void 0!==t?t:m[e]={}},module:function(e){return{id:e,uri:"",exports:m[e],config:function(e){return function(){return y&&y.config&&y.config[e]||{}}}(e)}}},o=function(e,t,n,r){var i,o,s,a,l,c,u,d=[],p=typeof n;if(c=S(r=r||e),"undefined"==p||"function"==p){for(t=!t.length&&n.length?["require","exports","module"]:t,l=0;l<t.length;l+=1)if("require"===(o=(a=f(t[l],c)).f))d[l]=g.require(e);else if("exports"===o)d[l]=g.exports(e),u=!0;else if("module"===o)i=d[l]=g.module(e);else if(w(m,o)||w(v,o)||w(_,o))d[l]=D(o);else{if(!a.p)throw new Error(e+" missing "+o);a.p.load(a.n,A(r,!0),x(o),{}),d[l]=m[o]}s=n?n.apply(m[e],d):void 0,e&&(i&&i.exports!==h&&i.exports!==m[e]?m[e]=i.exports:s===h&&u||(m[e]=s))}else e&&(m[e]=n)},t=n=s=function(e,t,n,r,i){if("string"==typeof e)return g[e]?g[e](t):D(f(e,S(t)).f);if(!e.splice){if((y=e).deps&&s(y.deps,y.callback),!t)return;t.splice?(e=t,t=n,n=null):e=h}return t=t||function(){},"function"==typeof n&&(n=r,r=i),r?o(h,e,t,n):setTimeout(function(){o(h,e,t,n)},4),s},s.config=function(e){return s(e)},t._defined=m,(r=function(e,t,n){if("string"!=typeof e)throw new Error("See almond README: incorrect module build, no module name");t.splice||(n=t,t=[]),w(m,e)||w(v,e)||(v[e]=[e,t,n])}).amd={jQuery:!0},e.requirejs=t,e.require=n,e.define=r),e.define("almond",function(){}),e.define("jquery",[],function(){var e=u||$;return null==e&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),e}),e.define("select2/utils",["jquery"],function(o){var i={};function u(e){var t=e.prototype,n=[];for(var r in t){"function"==typeof t[r]&&"constructor"!==r&&n.push(r)}return n}i.Extend=function(e,t){var n={}.hasOwnProperty;function r(){this.constructor=e}for(var i in t)n.call(t,i)&&(e[i]=t[i]);return r.prototype=t.prototype,e.prototype=new r,e.__super__=t.prototype,e},i.Decorate=function(r,i){var e=u(i),t=u(r);function o(){var e=Array.prototype.unshift,t=i.prototype.constructor.length,n=r.prototype.constructor;0<t&&(e.call(arguments,r.prototype.constructor),n=i.prototype.constructor),n.apply(this,arguments)}i.displayName=r.displayName,o.prototype=new function(){this.constructor=o};for(var n=0;n<t.length;n++){var s=t[n];o.prototype[s]=r.prototype[s]}function a(e){var t=function(){};e in o.prototype&&(t=o.prototype[e]);var n=i.prototype[e];return function(){return Array.prototype.unshift.call(arguments,t),n.apply(this,arguments)}}for(var l=0;l<e.length;l++){var c=e[l];o.prototype[c]=a(c)}return o};function e(){this.listeners={}}e.prototype.on=function(e,t){this.listeners=this.listeners||{},e in this.listeners?this.listeners[e].push(t):this.listeners[e]=[t]},e.prototype.trigger=function(e){var t=Array.prototype.slice,n=t.call(arguments,1);this.listeners=this.listeners||{},null==n&&(n=[]),0===n.length&&n.push({}),(n[0]._type=e)in this.listeners&&this.invoke(this.listeners[e],t.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},e.prototype.invoke=function(e,t){for(var n=0,r=e.length;n<r;n++)e[n].apply(this,t)},i.Observable=e,i.generateChars=function(e){for(var t="",n=0;n<e;n++){t+=Math.floor(36*Math.random()).toString(36)}return t},i.bind=function(e,t){return function(){e.apply(t,arguments)}},i._convertData=function(e){for(var t in e){var n=t.split("-"),r=e;if(1!==n.length){for(var i=0;i<n.length;i++){var o=n[i];(o=o.substring(0,1).toLowerCase()+o.substring(1))in r||(r[o]={}),i==n.length-1&&(r[o]=e[t]),r=r[o]}delete e[t]}}return e},i.hasScroll=function(e,t){var n=o(t),r=t.style.overflowX,i=t.style.overflowY;return(r!==i||"hidden"!==i&&"visible"!==i)&&("scroll"===r||"scroll"===i||(n.innerHeight()<t.scrollHeight||n.innerWidth()<t.scrollWidth))},i.escapeMarkup=function(e){var t={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('<ul class="select2-results__options" role="listbox"></ul>');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h('<li role="alert" aria-live="assertive" class="select2-results__option"></li>'),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n<e.results.length;n++){var r=e.results[n],i=this.option(r);t.push(i)}this.$results.append(t)}else 0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"})},r.prototype.position=function(e,t){t.find(".select2-results").append(e)},r.prototype.sort=function(e){return this.options.get("sorter")(e)},r.prototype.highlightFirstItem=function(){var e=this.$results.find(".select2-results__option[aria-selected]"),t=e.filter("[aria-selected=true]");0<t.length?t.first().trigger("mouseenter"):e.first().trigger("mouseenter"),this.ensureHighlightVisible()},r.prototype.setClasses=function(){var t=this;this.data.current(function(e){var r=h.map(e,function(e){return e.id.toString()});t.$results.find(".select2-results__option[aria-selected]").each(function(){var e=h(this),t=f.GetData(this,"data"),n=""+t.id;null!=t.element&&t.element.selected||null==t.element&&-1<h.inArray(n,r)?e.attr("aria-selected","true"):e.attr("aria-selected","false")})})},r.prototype.showLoading=function(e){this.hideLoading();var t={disabled:!0,loading:!0,text:this.options.get("translations").get("searching")(e)},n=this.option(t);n.className+=" loading-results",this.$results.prepend(n)},r.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},r.prototype.option=function(e){var t=document.createElement("li");t.className="select2-results__option";var n={role:"option","aria-selected":"false"},r=window.Element.prototype.matches||window.Element.prototype.msMatchesSelector||window.Element.prototype.webkitMatchesSelector;for(var i in(null!=e.element&&r.call(e.element,":disabled")||null==e.element&&e.disabled)&&(delete n["aria-selected"],n["aria-disabled"]="true"),null==e.id&&delete n["aria-selected"],null!=e._resultId&&(t.id=e._resultId),e.title&&(t.title=e.title),e.children&&(n.role="group",n["aria-label"]=e.text,delete n["aria-selected"]),n){var o=n[i];t.setAttribute(i,o)}if(e.children){var s=h(t),a=document.createElement("strong");a.className="select2-results__group";h(a);this.template(e,a);for(var l=[],c=0;c<e.children.length;c++){var u=e.children[c],d=this.option(u);l.push(d)}var p=h("<ul></ul>",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):i<o&&l.$results.scrollTop(s)}}),t.on("results:focus",function(e){e.element.addClass("select2-results__option--highlighted")}),t.on("results:message",function(e){l.displayMessage(e)}),h.fn.mousewheel&&this.$results.on("mousewheel",function(e){var t=l.$results.scrollTop(),n=l.$results.get(0).scrollHeight-t+e.deltaY,r=0<e.deltaY&&t-e.deltaY<=0,i=e.deltaY<0&&n<=l.$results.height();r?(l.$results.scrollTop(0),e.preventDefault(),e.stopPropagation()):i&&(l.$results.scrollTop(l.$results.get(0).scrollHeight-l.$results.height()),e.preventDefault(),e.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(e){var t=h(this),n=f.GetData(this,"data");"true"!==t.attr("aria-selected")?l.trigger("select",{originalEvent:e,data:n}):l.options.get("multiple")?l.trigger("unselect",{originalEvent:e,data:n}):l.trigger("close",{})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(e){var t=f.GetData(this,"data");l.getHighlightedResults().removeClass("select2-results__option--highlighted"),l.trigger("results:focus",{data:t,element:h(this)})})},r.prototype.getHighlightedResults=function(){return this.$results.find(".select2-results__option--highlighted")},r.prototype.destroy=function(){this.$results.remove()},r.prototype.ensureHighlightVisible=function(){var e=this.getHighlightedResults();if(0!==e.length){var t=this.$results.find("[aria-selected]").index(e),n=this.$results.offset().top,r=e.offset().top,i=this.$results.scrollTop()+(r-n),o=r-n;i-=2*e.outerHeight(!1),t<=2?this.$results.scrollTop(0):(o>this.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("<span></span>")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('<ul class="select2-selection__rendered"></ul>'),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.isDisabled()){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">×</span></li>')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n<e.length;n++){var r=e[n],i=this.selectionContainer(),o=this.display(r,i);i.append(o);var s=r.title||r.text;s&&i.attr("title",s),l.StoreData(i[0],"data",r),t.push(i)}var a=this.$selection.find(".select2-selection__rendered");l.appendMany(a,t)}},n}),e.define("select2/selection/placeholder",["../utils"],function(e){function t(e,t,n){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n)}return t.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},t.prototype.createPlaceholder=function(e,t){var n=this.selectionContainer();return n.html(this.display(t)),n.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),n},t.prototype.update=function(e,t){var n=1==t.length&&t[0].id!=this.placeholder.id;if(1<t.length||n)return e.call(this,t);this.clear();var r=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(r)},t}),e.define("select2/selection/allowClear",["jquery","../keys","../utils"],function(i,r,a){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(e){r._handleClear(e)}),t.on("keypress",function(e){r._handleKeyboardClear(e,t)})},e.prototype._handleClear=function(e,t){if(!this.isDisabled()){var n=this.$selection.find(".select2-selection__clear");if(0!==n.length){t.stopPropagation();var r=a.GetData(n[0],"data"),i=this.$element.val();this.$element.val(this.placeholder.id);var o={data:r};if(this.trigger("clear",o),o.prevented)this.$element.val(i);else{for(var s=0;s<r.length;s++)if(o={data:r[s]},this.trigger("unselect",o),o.prevented)return void this.$element.val(i);this.$element.trigger("input").trigger("change"),this.trigger("toggle",{})}}}},e.prototype._handleKeyboardClear=function(e,t,n){n.isOpen()||t.which!=r.DELETE&&t.which!=r.BACKSPACE||this._handleClear(t)},e.prototype.update=function(e,t){if(e.call(this,t),!(0<this.$selection.find(".select2-selection__placeholder").length||0===t.length)){var n=this.options.get("translations").get("removeAllItems"),r=i('<span class="select2-selection__clear" title="'+n()+'">×</span>');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></li>');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0<t.length){var n=a.GetData(t[0],"data");r.searchRemoveChoice(n),e.preventDefault()}}}),this.$selection.on("click",".select2-search--inline",function(e){r.$search.val()&&e.stopPropagation()});var o=document.documentMode,s=o&&o<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){s?r.$selection.off("input.search input.searchcheck"):r.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){if(s&&"input"===e.type)r.$selection.off("input.search input.searchcheck");else{var t=e.which;t!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&r.handleSearch(e)}})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var e=this.$search.val();this.trigger("query",{term:e})}this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="";""!==this.$search.attr("placeholder")?e=this.$selection.find(".select2-selection__rendered").width():e=.75*(this.$search.val().length+1)+"em";this.$search.css("width",e)},e}),e.define("select2/selection/eventRelay",["jquery"],function(s){function e(){}return e.prototype.bind=function(e,t,n){var r=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],o=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){if(-1!==s.inArray(e,i)){t=t||{};var n=s.Event("select2:"+e,{params:t});r.$element.trigger(n),-1!==s.inArray(e,o)&&(t.prevented=n.isDefaultPrevented())}})},e}),e.define("select2/translation",["jquery","require"],function(t,n){function r(e){this.dict=e||{}}return r.prototype.all=function(){return this.dict},r.prototype.get=function(e){return this.dict[e]},r.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},r._cache={},r.loadPath=function(e){if(!(e in r._cache)){var t=n(e);r._cache[e]=t}return new r(r._cache[e])},r}),e.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),e.define("select2/data/base",["../utils"],function(r){function n(e,t){n.__super__.constructor.call(this)}return r.Extend(n,r.Observable),n.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},n.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},n.prototype.bind=function(e,t){},n.prototype.destroy=function(){},n.prototype.generateResultId=function(e,t){var n=e.id+"-result-";return n+=r.generateChars(4),null!=t.id?n+="-"+t.id.toString():n+="-"+r.generateChars(4),n},n}),e.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var n=[],r=this;this.$element.find(":selected").each(function(){var e=l(this),t=r.item(e);n.push(t)}),e(n)},n.prototype.select=function(i){var o=this;if(i.selected=!0,l(i.element).is("option"))return i.element.selected=!0,void this.$element.trigger("input").trigger("change");if(this.$element.prop("multiple"))this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;n<i.length;n++){var r=i[n].id;-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("input").trigger("change")});else{var e=i.id;this.$element.val(e),this.$element.trigger("input").trigger("change")}},n.prototype.unselect=function(i){var o=this;if(this.$element.prop("multiple")){if(i.selected=!1,l(i.element).is("option"))return i.element.selected=!1,void this.$element.trigger("input").trigger("change");this.current(function(e){for(var t=[],n=0;n<e.length;n++){var r=e[n].id;r!==i.id&&-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("input").trigger("change")})}},n.prototype.bind=function(e,t){var n=this;(this.container=e).on("select",function(e){n.select(e.data)}),e.on("unselect",function(e){n.unselect(e.data)})},n.prototype.destroy=function(){this.$element.find("*").each(function(){a.RemoveData(this)})},n.prototype.query=function(r,e){var i=[],o=this;this.$element.children().each(function(){var e=l(this);if(e.is("option")||e.is("optgroup")){var t=o.item(e),n=o.matches(r,t);null!==n&&i.push(n)}}),e({results:i})},n.prototype.addOptions=function(e){a.appendMany(this.$element,e)},n.prototype.option=function(e){var t;e.children?(t=document.createElement("optgroup")).label=e.text:void 0!==(t=document.createElement("option")).textContent?t.textContent=e.text:t.innerText=e.text,void 0!==e.id&&(t.value=e.id),e.disabled&&(t.disabled=!0),e.selected&&(t.selected=!0),e.title&&(t.title=e.title);var n=l(t),r=this._normalizeItem(e);return r.element=t,a.StoreData(t,"data",r),n},n.prototype.item=function(e){var t={};if(null!=(t=a.GetData(e[0],"data")))return t;if(e.is("option"))t={id:e.val(),text:e.text(),disabled:e.prop("disabled"),selected:e.prop("selected"),title:e.prop("title")};else if(e.is("optgroup")){t={text:e.prop("label"),children:[],title:e.prop("title")};for(var n=e.children("option"),r=[],i=0;i<n.length;i++){var o=l(n[i]),s=this.item(o);r.push(s)}t.children=r}return(t=this._normalizeItem(t)).element=e[0],a.StoreData(e[0],"data",t),t},n.prototype._normalizeItem=function(e){e!==Object(e)&&(e={id:e,text:e});return null!=(e=l.extend({},{text:""},e)).id&&(e.id=e.id.toString()),null!=e.text&&(e.text=e.text.toString()),null==e._resultId&&e.id&&null!=this.container&&(e._resultId=this.generateResultId(this.container,e)),l.extend({},{selected:!1,disabled:!1},e)},n.prototype.matches=function(e,t){return this.options.get("matcher")(e,t)},n}),e.define("select2/data/array",["./select","../utils","jquery"],function(e,f,g){function r(e,t){this._dataToConvert=t.get("data")||[],r.__super__.constructor.call(this,e,t)}return f.Extend(r,e),r.prototype.bind=function(e,t){r.__super__.bind.call(this,e,t),this.addOptions(this.convertToOptions(this._dataToConvert))},r.prototype.select=function(n){var e=this.$element.find("option").filter(function(e,t){return t.value==n.id.toString()});0===e.length&&(e=this.option(n),this.addOptions(e)),r.__super__.select.call(this,n)},r.prototype.convertToOptions=function(e){var t=this,n=this.$element.find("option"),r=n.map(function(){return t.item(g(this)).id}).get(),i=[];function o(e){return function(){return g(this).val()==e.id}}for(var s=0;s<e.length;s++){var a=this._normalizeItem(e[s]);if(0<=g.inArray(a.id,r)){var l=n.filter(o(a)),c=this.item(l),u=g.extend(!0,{},a,c),d=this.option(u);l.replaceWith(d)}else{var p=this.option(a);if(a.children){var h=this.convertToOptions(a.children);f.appendMany(p,h)}i.push(p)}}return i},r}),e.define("select2/data/ajax",["./array","../utils","jquery"],function(e,t,o){function n(e,t){this.ajaxOptions=this._applyDefaults(t.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),n.__super__.constructor.call(this,e,t)}return t.Extend(n,e),n.prototype._applyDefaults=function(e){var t={data:function(e){return o.extend({},e,{q:e.term})},transport:function(e,t,n){var r=o.ajax(e);return r.then(t),r.fail(n),r}};return o.extend({},t,e,!0)},n.prototype.processResults=function(e){return e},n.prototype.query=function(n,r){var i=this;null!=this._request&&(o.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var t=o.extend({type:"GET"},this.ajaxOptions);function e(){var e=t.transport(t,function(e){var t=i.processResults(e,n);i.options.get("debug")&&window.console&&console.error&&(t&&t.results&&o.isArray(t.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),r(t)},function(){"status"in e&&(0===e.status||"0"===e.status)||i.trigger("results:message",{message:"errorLoading"})});i._request=e}"function"==typeof t.url&&(t.url=t.url.call(this.$element,n)),"function"==typeof t.data&&(t.data=t.data.call(this.$element,n)),this.ajaxOptions.delay&&null!=n.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(e,this.ajaxOptions.delay)):e()},n}),e.define("select2/data/tags",["jquery"],function(u){function e(e,t,n){var r=n.get("tags"),i=n.get("createTag");void 0!==i&&(this.createTag=i);var o=n.get("insertTag");if(void 0!==o&&(this.insertTag=o),e.call(this,t,n),u.isArray(r))for(var s=0;s<r.length;s++){var a=r[s],l=this._normalizeItem(a),c=this.option(l);this.$element.append(c)}}return e.prototype.query=function(e,c,u){var d=this;this._removeOldTags(),null!=c.term&&null==c.page?e.call(this,c,function e(t,n){for(var r=t.results,i=0;i<r.length;i++){var o=r[i],s=null!=o.children&&!e({results:o.children},!0);if((o.text||"").toUpperCase()===(c.term||"").toUpperCase()||s)return!n&&(t.data=r,void u(t))}if(n)return!0;var a=d.createTag(c);if(null!=a){var l=d.option(a);l.attr("data-select2-tag",!0),d.addOptions([l]),d.insertTag(r,a)}t.results=r,u(t)}):e.call(this,c,u)},e.prototype.createTag=function(e,t){var n=u.trim(t.term);return""===n?null:{id:n,text:n}},e.prototype.insertTag=function(e,t,n){t.unshift(n)},e.prototype._removeOldTags=function(e){this.$element.find("option[data-select2-tag]").each(function(){this.selected||u(this).remove()})},e}),e.define("select2/data/tokenizer",["jquery"],function(d){function e(e,t,n){var r=n.get("tokenizer");void 0!==r&&(this.tokenizer=r),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){e.call(this,t,n),this.$search=t.dropdown.$search||t.selection.$search||n.find(".select2-search__field")},e.prototype.query=function(e,t,n){var r=this;t.term=t.term||"";var i=this.tokenizer(t,this.options,function(e){var t=r._normalizeItem(e);if(!r.$element.find("option").filter(function(){return d(this).val()===t.id}).length){var n=r.option(t);n.attr("data-select2-tag",!0),r._removeOldTags(),r.addOptions([n])}!function(e){r.trigger("select",{data:e})}(t)});i.term!==t.term&&(this.$search.length&&(this.$search.val(i.term),this.$search.trigger("focus")),t.term=i.term),e.call(this,t,n)},e.prototype.tokenizer=function(e,t,n,r){for(var i=n.get("tokenSeparators")||[],o=t.term,s=0,a=this.createTag||function(e){return{id:e.term,text:e.term}};s<o.length;){var l=o[s];if(-1!==d.inArray(l,i)){var c=o.substr(0,s),u=a(d.extend({},t,{term:c}));null!=u?(r(u),o=o.substr(s+1)||"",s=0):s++}else s++}return{term:o}},e}),e.define("select2/data/minimumInputLength",[],function(){function e(e,t,n){this.minimumInputLength=n.get("minimumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",t.term.length<this.minimumInputLength?this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumInputLength",[],function(){function e(e,t,n){this.maximumInputLength=n.get("maximumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",0<this.maximumInputLength&&t.term.length>this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0<r.maximumSelectionLength&&t>=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('<span class="select2-dropdown"><span class="select2-results"></span></span>');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></span>');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('<li class="select2-results__option select2-results__option--load-more"role="option" aria-disabled="true"></li>'),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f("<span></span>"),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=a<i.top-s,u=l>i.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r<t.length;r++){var i=t[r];i.children?n+=e(i.children):n++}return n}(t.data.results)<this.minimumResultsForSearch)&&e.call(this,t)},e}),e.define("select2/dropdown/selectOnClose",["../utils"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("close",function(e){r._handleSelectOnClose(e)})},e.prototype._handleSelectOnClose=function(e,t){if(t&&null!=t.originalSelect2Event){var n=t.originalSelect2Event;if("select"===n._type||"unselect"===n._type)return}var r=this.getHighlightedResults();if(!(r.length<1)){var i=o.GetData(r[0],"data");null!=i.element&&i.element.selected||null==i.element&&i.selected||this.trigger("select",{data:i})}},e}),e.define("select2/dropdown/closeOnSelect",[],function(){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(e){r._selectTriggered(e)}),t.on("unselect",function(e){r._selectTriggered(e)})},e.prototype._selectTriggered=function(e,t){var n=t.originalEvent;n&&(n.ctrlKey||n.metaKey)||this.trigger("close",{originalEvent:n,originalSelect2Event:t})},e}),e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Please delete "+t+" character";return 1!=t&&(n+="s"),n},inputTooShort:function(e){return"Please enter "+(e.minimum-e.input.length)+" or more characters"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var t="You can only select "+e.maximum+" item";return 1!=e.maximum&&(t+="s"),t},noResults:function(){return"No results found"},searching:function(){return"Searching…"},removeAllItems:function(){return"Remove all items"}}}),e.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(c,u,d,p,h,f,g,m,v,y,s,t,_,$,b,w,A,x,D,S,E,C,O,T,q,L,I,j,e){function n(){this.reset()}return n.prototype.apply=function(e){if(null==(e=c.extend(!0,{},this.defaults,e)).dataAdapter){if(null!=e.ajax?e.dataAdapter=b:null!=e.data?e.dataAdapter=$:e.dataAdapter=_,0<e.minimumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,x)),0<e.maximumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,D)),0<e.maximumSelectionLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,S)),e.tags&&(e.dataAdapter=y.Decorate(e.dataAdapter,w)),null==e.tokenSeparators&&null==e.tokenizer||(e.dataAdapter=y.Decorate(e.dataAdapter,A)),null!=e.query){var t=u(e.amdBase+"compat/query");e.dataAdapter=y.Decorate(e.dataAdapter,t)}if(null!=e.initSelection){var n=u(e.amdBase+"compat/initSelection");e.dataAdapter=y.Decorate(e.dataAdapter,n)}}if(null==e.resultsAdapter&&(e.resultsAdapter=d,null!=e.ajax&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,T)),null!=e.placeholder&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,O)),e.selectOnClose&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,I))),null==e.dropdownAdapter){if(e.multiple)e.dropdownAdapter=E;else{var r=y.Decorate(E,C);e.dropdownAdapter=r}if(0!==e.minimumResultsForSearch&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,L)),e.closeOnSelect&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,j)),null!=e.dropdownCssClass||null!=e.dropdownCss||null!=e.adaptDropdownCssClass){var i=u(e.amdBase+"compat/dropdownCss");e.dropdownAdapter=y.Decorate(e.dropdownAdapter,i)}e.dropdownAdapter=y.Decorate(e.dropdownAdapter,q)}if(null==e.selectionAdapter){if(e.multiple?e.selectionAdapter=h:e.selectionAdapter=p,null!=e.placeholder&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,f)),e.allowClear&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,g)),e.multiple&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,m)),null!=e.containerCssClass||null!=e.containerCss||null!=e.adaptContainerCssClass){var o=u(e.amdBase+"compat/containerCss");e.selectionAdapter=y.Decorate(e.selectionAdapter,o)}e.selectionAdapter=y.Decorate(e.selectionAdapter,v)}e.language=this._resolveLanguage(e.language),e.language.push("en");for(var s=[],a=0;a<e.language.length;a++){var l=e.language[a];-1===s.indexOf(l)&&s.push(l)}return e.language=s,e.translations=this._processTranslations(e.language,e.debug),e},n.prototype.reset=function(){function a(e){return e.replace(/[^\u0000-\u007E]/g,function(e){return t[e]||e})}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:y.escapeMarkup,language:{},matcher:function e(t,n){if(""===c.trim(t.term))return n;if(n.children&&0<n.children.length){for(var r=c.extend(!0,{},n),i=n.children.length-1;0<=i;i--)null==e(t,n.children[i])&&r.children.splice(i,1);return 0<r.children.length?r:e(t,r)}var o=a(n.text).toUpperCase(),s=a(t.term).toUpperCase();return-1<o.indexOf(s)?n:null},minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,scrollAfterSelect:!1,sorter:function(e){return e},templateResult:function(e){return e.text},templateSelection:function(e){return e.text},theme:"default",width:"resolve"}},n.prototype.applyFromElement=function(e,t){var n=e.language,r=this.defaults.language,i=t.prop("lang"),o=t.closest("[lang]").prop("lang"),s=Array.prototype.concat.call(this._resolveLanguage(i),this._resolveLanguage(n),this._resolveLanguage(r),this._resolveLanguage(o));return e.language=s,e},n.prototype._resolveLanguage=function(e){if(!e)return[];if(c.isEmptyObject(e))return[];if(c.isPlainObject(e))return[e];var t;t=c.isArray(e)?e:[e];for(var n=[],r=0;r<t.length;r++)if(n.push(t[r]),"string"==typeof t[r]&&0<t[r].indexOf("-")){var i=t[r].split("-")[0];n.push(i)}return n},n.prototype._processTranslations=function(e,t){for(var n=new s,r=0;r<e.length;r++){var i=new s,o=e[r];if("string"==typeof o)try{i=s.loadPath(o)}catch(e){try{o=this.defaults.amdLanguageBase+o,i=s.loadPath(o)}catch(e){t&&window.console&&console.warn&&console.warn('Select2: The language file for "'+o+'" could not be automatically loaded. A fallback will be used instead.')}}else i=c.isPlainObject(o)?new s(o):o;n.extend(i)}return n},n.prototype.set=function(e,t){var n={};n[c.camelCase(e)]=t;var r=y._convertData(n);c.extend(!0,this.defaults,r)},new n}),e.define("select2/options",["require","jquery","./defaults","./utils"],function(r,d,i,p){function e(e,t){if(this.options=e,null!=t&&this.fromElement(t),null!=t&&(this.options=i.applyFromElement(this.options,t)),this.options=i.apply(this.options),t&&t.is("input")){var n=r(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=p.Decorate(this.options.dataAdapter,n)}}return e.prototype.fromElement=function(e){var t=["select2"];null==this.options.multiple&&(this.options.multiple=e.prop("multiple")),null==this.options.disabled&&(this.options.disabled=e.prop("disabled")),null==this.options.dir&&(e.prop("dir")?this.options.dir=e.prop("dir"):e.closest("[dir]").prop("dir")?this.options.dir=e.closest("[dir]").prop("dir"):this.options.dir="ltr"),e.prop("disabled",this.options.disabled),e.prop("multiple",this.options.multiple),p.GetData(e[0],"select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),p.StoreData(e[0],"data",p.GetData(e[0],"select2Tags")),p.StoreData(e[0],"tags",!0)),p.GetData(e[0],"ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),e.attr("ajax--url",p.GetData(e[0],"ajaxUrl")),p.StoreData(e[0],"ajax-Url",p.GetData(e[0],"ajaxUrl")));var n={};function r(e,t){return t.toUpperCase()}for(var i=0;i<e[0].attributes.length;i++){var o=e[0].attributes[i].name,s="data-";if(o.substr(0,s.length)==s){var a=o.substring(s.length),l=p.GetData(e[0],a);n[a.replace(/-([a-z])/g,r)]=l}}d.fn.jquery&&"1."==d.fn.jquery.substr(0,2)&&e[0].dataset&&(n=d.extend(!0,{},e[0].dataset,n));var c=d.extend(!0,{},p.GetData(e[0]),n);for(var u in c=p._convertData(c))-1<d.inArray(u,t)||(d.isPlainObject(this.options[u])?d.extend(this.options[u],c[u]):this.options[u]=c[u]);return this},e.prototype.get=function(e){return this.options[e]},e.prototype.set=function(e,t){this.options[e]=t},e}),e.define("select2/core",["jquery","./options","./utils","./keys"],function(o,c,u,r){var d=function(e,t){null!=u.GetData(e[0],"select2")&&u.GetData(e[0],"select2").destroy(),this.$element=e,this.id=this._generateId(e),t=t||{},this.options=new c(t,e),d.__super__.constructor.call(this);var n=e.attr("tabindex")||0;u.StoreData(e[0],"old-tabindex",n),e.attr("tabindex","-1");var r=this.options.get("dataAdapter");this.dataAdapter=new r(e,this.options);var i=this.render();this._placeContainer(i);var o=this.options.get("selectionAdapter");this.selection=new o(e,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,i);var s=this.options.get("dropdownAdapter");this.dropdown=new s(e,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,i);var a=this.options.get("resultsAdapter");this.results=new a(e,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var l=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(e){l.trigger("selection:update",{data:e})}),e.addClass("select2-hidden-accessible"),e.attr("aria-hidden","true"),this._syncAttributes(),u.StoreData(e[0],"select2",this),e.data("select2",this)};return u.Extend(d,u.Observable),d.prototype._generateId=function(e){return"select2-"+(null!=e.attr("id")?e.attr("id"):null!=e.attr("name")?e.attr("name")+"-"+u.generateChars(2):u.generateChars(4)).replace(/(:|\.|\[|\]|,)/g,"")},d.prototype._placeContainer=function(e){e.insertAfter(this.$element);var t=this._resolveWidth(this.$element,this.options.get("width"));null!=t&&e.css("width",t)},d.prototype._resolveWidth=function(e,t){var n=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==t){var r=this._resolveWidth(e,"style");return null!=r?r:this._resolveWidth(e,"element")}if("element"==t){var i=e.outerWidth(!1);return i<=0?"auto":i+"px"}if("style"!=t)return"computedstyle"!=t?t:window.getComputedStyle(e[0]).width;var o=e.attr("style");if("string"!=typeof o)return null;for(var s=o.split(";"),a=0,l=s.length;a<l;a+=1){var c=s[a].replace(/\s/g,"").match(n);if(null!==c&&1<=c.length)return c[1]}return null},d.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},d.prototype._registerDomEvents=function(){var t=this;this.$element.on("change.select2",function(){t.dataAdapter.current(function(e){t.trigger("selection:update",{data:e})})}),this.$element.on("focus.select2",function(e){t.trigger("focus",e)}),this._syncA=u.bind(this._syncAttributes,this),this._syncS=u.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var e=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=e?(this._observer=new e(function(e){t._syncA(),t._syncS(null,e)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",t._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",t._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",t._syncS,!1))},d.prototype._registerDataEvents=function(){var n=this;this.dataAdapter.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerSelectionEvents=function(){var n=this,r=["toggle","focus"];this.selection.on("toggle",function(){n.toggleDropdown()}),this.selection.on("focus",function(e){n.focus(e)}),this.selection.on("*",function(e,t){-1===o.inArray(e,r)&&n.trigger(e,t)})},d.prototype._registerDropdownEvents=function(){var n=this;this.dropdown.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerResultsEvents=function(){var n=this;this.results.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerEvents=function(){var n=this;this.on("open",function(){n.$container.addClass("select2-container--open")}),this.on("close",function(){n.$container.removeClass("select2-container--open")}),this.on("enable",function(){n.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){n.$container.addClass("select2-container--disabled")}),this.on("blur",function(){n.$container.removeClass("select2-container--focus")}),this.on("query",function(t){n.isOpen()||n.trigger("open",{}),this.dataAdapter.query(t,function(e){n.trigger("results:all",{data:e,query:t})})}),this.on("query:append",function(t){this.dataAdapter.query(t,function(e){n.trigger("results:append",{data:e,query:t})})}),this.on("keypress",function(e){var t=e.which;n.isOpen()?t===r.ESC||t===r.TAB||t===r.UP&&e.altKey?(n.close(e),e.preventDefault()):t===r.ENTER?(n.trigger("results:select",{}),e.preventDefault()):t===r.SPACE&&e.ctrlKey?(n.trigger("results:toggle",{}),e.preventDefault()):t===r.UP?(n.trigger("results:previous",{}),e.preventDefault()):t===r.DOWN&&(n.trigger("results:next",{}),e.preventDefault()):(t===r.ENTER||t===r.SPACE||t===r.DOWN&&e.altKey)&&(n.open(),e.preventDefault())})},d.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.isDisabled()?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},d.prototype._isChangeMutation=function(e,t){var n=!1,r=this;if(!e||!e.target||"OPTION"===e.target.nodeName||"OPTGROUP"===e.target.nodeName){if(t)if(t.addedNodes&&0<t.addedNodes.length)for(var i=0;i<t.addedNodes.length;i++){t.addedNodes[i].selected&&(n=!0)}else t.removedNodes&&0<t.removedNodes.length?n=!0:o.isArray(t)&&o.each(t,function(e,t){if(r._isChangeMutation(e,t))return!(n=!0)});else n=!0;return n}},d.prototype._syncSubtree=function(e,t){var n=this._isChangeMutation(e,t),r=this;n&&this.dataAdapter.current(function(e){r.trigger("selection:update",{data:e})})},d.prototype.trigger=function(e,t){var n=d.__super__.trigger,r={open:"opening",close:"closing",select:"selecting",unselect:"unselecting",clear:"clearing"};if(void 0===t&&(t={}),e in r){var i=r[e],o={prevented:!1,name:e,args:t};if(n.call(this,i,o),o.prevented)return void(t.prevented=!0)}n.call(this,e,t)},d.prototype.toggleDropdown=function(){this.isDisabled()||(this.isOpen()?this.close():this.open())},d.prototype.open=function(){this.isOpen()||this.isDisabled()||this.trigger("query",{})},d.prototype.close=function(e){this.isOpen()&&this.trigger("close",{originalEvent:e})},d.prototype.isEnabled=function(){return!this.isDisabled()},d.prototype.isDisabled=function(){return this.options.get("disabled")},d.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},d.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},d.prototype.focus=function(e){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},d.prototype.enable=function(e){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),null!=e&&0!==e.length||(e=[!0]);var t=!e[0];this.$element.prop("disabled",t)},d.prototype.data=function(){this.options.get("debug")&&0<arguments.length&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var t=[];return this.dataAdapter.current(function(e){t=e}),t},d.prototype.val=function(e){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==e||0===e.length)return this.$element.val();var t=e[0];o.isArray(t)&&(t=o.map(t,function(e){return e.toString()})),this.$element.val(t).trigger("input").trigger("change")},d.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",u.GetData(this.$element[0],"old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),u.RemoveData(this.$element[0]),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},d.prototype.render=function(){var e=o('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1<i.inArray(t,a)?this:n}}return null==i.fn.select2.defaults&&(i.fn.select2.defaults=t),o}),{define:e.define,require:e.require}}(),t=e.require("jquery.select2");return u.fn.select2.amd=e,t});
@@ -1,6 +1,6 @@ cabal-version: 2.2 name: ihp-version: 1.4.1+version: 1.5.0 synopsis: Haskell Web Framework description: The Integrated Haskell Platform is a full stack framework focused on rapid application development while striving for robust code quality. Full guide available at: <https://ihp.digitallyinduced.com/Guide/index.html>@@ -22,28 +22,15 @@ , static/*.svg , static/vendor/*.min.css- , static/vendor/*.min.css.map , static/vendor/*.min.js- , static/vendor/*.min.js.map , static/vendor/*.js - , static/vendor/jquery-3.2.1.slim.min.js- , static/vendor/jquery-3.5.0.min.js- , static/vendor/jquery-3.6.0.min.js- , static/vendor/jquery-3.6.0.slim.min.js , static/vendor/popper-2.11.6.min.js - , static/vendor/bootstrap-5.2.1/*.min.css- , static/vendor/bootstrap-5.2.1/*.min.js- , static/vendor/src-min/*.js , static/vendor/src-min/snippets/*.js extra-source-files: CHANGELOG.md -Flag FastBuild- Default: False- Description: Disables all optimisations, leads to faster build time- common shared-properties default-language: GHC2021 build-depends:@@ -51,7 +38,8 @@ , classy-prelude , mono-traversable , transformers- , directory+ , directory >= 1.3.8.0+ , filepath >= 1.5 , string-conversions , warp , warp-systemd@@ -64,14 +52,15 @@ , inflections , text , postgresql-simple+ , ihp-pglistener , wai-app-static , wai-util , bytestring , network-uri , uri-encode , aeson- , wai-session- , wai-session-clientsession+ , wai-session-maybe+ , wai-session-clientsession-deferred , clientsession , pwstore-fast , vault@@ -101,19 +90,16 @@ , wai-websockets , regex-tdfa , binary- , mime-mail- , mime-mail-ses- , smtp-mail , http-client , http-client-tls , resource-pool , unordered-containers+ , hashable , scientific , vector , wreq , deepseq , parser-combinators- , ip , fast-logger , minio-hs , temporary@@ -128,11 +114,30 @@ , unagi-chan , with-utf8 , ihp-hsx- , ihp-postgresql-simple-extra+ , ihp-context+ , ihp-pagehead+ , ihp-log+ , ihp-modal , mime-types , safe-exceptions , resourcet , countable-inflections+ , stm+ , wai-asset-path+ , wai-flash-messages+ , slugger+ , ihp-imagemagick+ , wai-request-params+ , hasql+ , hasql-pool+ , hasql-dynamic-statements+ , hasql-implicits+ , hasql-mapping+ , hasql-postgresql-types+ , hasql-transaction+ , postgresql-types+ , postgresql-simple-postgresql-types+ , contravariant default-extensions: OverloadedStrings , NoImplicitPrelude@@ -153,42 +158,19 @@ , TemplateHaskell , OverloadedRecordDot , DeepSubsumption- if flag(FastBuild)- ghc-options:- -threaded- +RTS -A256m -n4m -H512m -qg -N -RTS- -Wunused-imports- -Wunused-foralls- -Wmissing-fields- -Winaccessible-code- -Wmissed-specialisations- -Wall-missed-specialisations- -Wno-ambiguous-fields- else- ghc-options:- -fstatic-argument-transformation- -funbox-strict-fields- -haddock- +RTS -A256m -n4m -H512m -qg -N -RTS- -Wunused-imports- -Wunused-foralls- -Wmissing-fields- -Winaccessible-code- -Wmissed-specialisations- -Wall-missed-specialisations- -Wno-ambiguous-fields+ ghc-options: -Werror=incomplete-patterns -Werror=unused-imports -Werror=missing-fields library import: shared-properties hs-source-dirs: . exposed-modules:- IHP.AuthSupport.Authentication+ IHP.ActionType+ , IHP.AuthSupport.Authentication , IHP.AuthSupport.Lockable , IHP.Controller.Param , IHP.Controller.FileUpload , IHP.Controller.Redirect , IHP.Controller.Render- , IHP.Controller.RequestContext , IHP.Controller.Session , IHP.Controller.Context , IHP.Controller.Layout@@ -205,32 +187,46 @@ , IHP.ValidationSupport.ValidateField , IHP.ValidationSupport.ValidateIsUnique , IHP.View.Form- , IHP.Modal.Types- , IHP.Modal.ControllerFunctions- , IHP.Modal.ViewFunctions+ , IHP.View.Form.Fields+ , IHP.View.Form.FormFor+ , IHP.View.Form.Select , IHP.View.TimeAgo- , IHP.Welcome.Controller- , IHP.ApplicationContext+ , IHP.RequestVault+ , IHP.RequestVault.Helper+ , IHP.RequestVault.ModelContext , IHP.ControllerPrelude , IHP.ControllerSupport , IHP.Environment , IHP.FetchRelated , IHP.FrameworkConfig+ , IHP.FrameworkConfig.Types , IHP.HaskellSupport- , IHP.Log- , IHP.Log.Types+ , IHP.Record+ , IHP.InputValue , IHP.ModelSupport+ , IHP.ModelSupport.Types , IHP.NameSupport , IHP.QueryBuilder+ , IHP.QueryBuilder.Types+ , IHP.QueryBuilder.Compiler+ , IHP.QueryBuilder.Filter+ , IHP.QueryBuilder.Join+ , IHP.QueryBuilder.Order+ , IHP.QueryBuilder.Union+ , IHP.QueryBuilder.HasqlCompiler+ , IHP.QueryBuilder.HasqlHelpers , IHP.Fetch+ , IHP.Fetch.Statement+ , IHP.FetchPipelined , IHP.RouterPrelude , IHP.Server+ , IHP.Static , IHP.ValidationSupport- , IHP.ViewErrorMessages , IHP.ViewPrelude , IHP.ViewSupport , IHP.RouterSupport , IHP.Router.Types+ , IHP.Router.UrlGenerator , IHP.Prelude , IHP.ErrorController , IHP.ScriptSupport@@ -240,57 +236,94 @@ , IHP.AutoRefresh.Types , IHP.AutoRefresh.View , IHP.WebSocket- , IHP.Mail- , IHP.Mail.Types- , IHP.MailPrelude , IHP.View.Classes , IHP.View.CSSFramework+ , IHP.View.CSSFramework.Unstyled+ , IHP.View.CSSFramework.Bootstrap+ , IHP.View.CSSFramework.Tailwind , IHP.View.Types- , IHP.FlashMessages.Types- , IHP.FlashMessages.ControllerFunctions- , IHP.FlashMessages.ViewFunctions- , IHP.SchemaMigration+ , IHP.FlashMessages , IHP.Test.Mocking , IHP.Job.Queue , IHP.Job.Runner , IHP.Job.Types- , IHP.Job.Dashboard- , IHP.Job.Dashboard.Types- , IHP.Job.Dashboard.Auth- , IHP.Job.Dashboard.View- , IHP.Job.Dashboard.Utils- , IHP.PageHead.ControllerFunctions- , IHP.PageHead.Types- , IHP.PageHead.ViewFunctions- , IHP.Assets.Types- , IHP.Assets.ViewFunctions+ , IHP.Job.Types.BackoffStrategy+ , IHP.Job.Types.Class+ , IHP.Job.Types.Status+ , IHP.Job.Types.Worker+ , IHP.Job.Queue.Pool+ , IHP.Job.Queue.Fetch+ , IHP.Job.Queue.Watch+ , IHP.Job.Queue.Result+ , IHP.Job.Queue.StatusInstances+ , IHP.Job.Queue.STM+ , IHP.Job.Runner.MainLoop+ , IHP.Job.Runner.WorkerLoop , IHP.Breadcrumb.Types , IHP.Breadcrumb.ViewFunctions , IHP.FileStorage.Types , IHP.FileStorage.ControllerFunctions , IHP.FileStorage.Config- , IHP.FileStorage.Preprocessor.ImageMagick , IHP.Pagination.ControllerFunctions , IHP.Pagination.ViewFunctions , IHP.Pagination.Types , IHP.Pagination.Helpers- -- DataSync- , IHP.DataSync.Types- , IHP.DataSync.RowLevelSecurity- , IHP.DataSync.DynamicQuery- , IHP.DataSync.DynamicQueryCompiler- , IHP.DataSync.ChangeNotifications- , IHP.DataSync.Controller- , IHP.DataSync.ControllerImpl- , IHP.DataSync.REST.Types- , IHP.DataSync.REST.Routes- , IHP.DataSync.REST.Controller- , IHP.DataSync.Role- , IHP.PGListener , IHP.EnvVar+ , IHP.Hasql.FromRow+ , IHP.Hasql.Encoders+ , IHP.Hasql.Pool+ , IHP.PGSimpleCompat+ , IHP.PGVersion+ reexported-modules: IHP.PGListener autogen-modules: Paths_ihp- other-modules: Paths_ihp+ other-modules:+ Paths_ihp source-repository head type: git location: https://github.com/digitallyinduced/ihp++test-suite tests+ import: shared-properties+ type: exitcode-stdio-1.0+ main-is: Test/Main.hs+ hs-source-dirs: Test+ ghc-options: -threaded+ build-depends: ihp+ other-modules:+ Test.ValidationSupport.ValidateFieldSpec+ Test.NameSupportSpec+ Test.HaskellSupportSpec+ Test.View.CSSFrameworkSpec+ Test.View.FormSpec+ Test.Controller.ContextSpec+ Test.Controller.ParamSpec+ Test.Controller.AccessDeniedSpec+ Test.Controller.NotFoundSpec+ Test.ModelSupportSpec+ Test.QueryBuilderSpec+ Test.RouterSupportSpec+ Test.ViewSupportSpec+ Test.FileStorage.ControllerFunctionsSpec+ Test.Controller.CookieSpec+ Test.PGListenerSpec+ Test.MockingSpec+ Test.ControllerSupportSpec+ Test.HasqlEncoderSpec+ Test.AutoRefreshSpec+ Test.Pagination.ControllerFunctionsSpec+ Test.FetchPipelinedSpec+ Test.JobQueueSpec+ Test.ModelFixtures++benchmark ihp-router-bench+ import: shared-properties+ type: exitcode-stdio-1.0+ hs-source-dirs: .+ main-is: Test/RouterBench.hs+ -- tasty-bench requires -fno-static-argument-transformation to prevent+ -- GHC from defeating the re-evaluation trick used for measurement+ ghc-options: -fno-static-argument-transformation+ build-depends:+ , ihp+ , tasty-bench