vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit a1c6197b1d5774f2ef7debebff7a19678805bd51
parent a949dda7efd1f6c8b7d04ed4d8380248c5b20c00
Author: Zack Newman <zack@philomathiclife.com>
Date:   Thu, 22 Aug 2024 17:17:22 -0600

integrate relevant changes from upstream

Diffstat:
Msrc/api/core/accounts.rs | 428++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/api/core/ciphers.rs | 499++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/api/core/emergency_access.rs | 86++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/api/core/events.rs | 30++++++++++++++----------------
Msrc/api/core/folders.rs | 27++++++++++++++-------------
Msrc/api/core/mod.rs | 52+++++++++++++++++++++-------------------------------
Msrc/api/core/organizations.rs | 747++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/api/core/public.rs | 27++++++++++++++++-----------
Msrc/api/core/sends.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/api/core/two_factor/authenticator.rs | 46+++++++++++++++++++++++-----------------------
Msrc/api/core/two_factor/duo.rs | 31++++++++++++++++---------------
Msrc/api/core/two_factor/email.rs | 51+++++++++++++++++++++++++++++----------------------
Msrc/api/core/two_factor/mod.rs | 50+++++++++++++++++++++++++++-----------------------
Msrc/api/core/two_factor/protected_actions.rs | 10+++++-----
Msrc/api/core/two_factor/webauthn.rs | 52++++++++++++++++++++++------------------------------
Msrc/api/core/two_factor/yubikey.rs | 40++++++++++++++++++++++------------------
Msrc/api/identity.rs | 33+++++++++++++++++++++++----------
Msrc/api/mod.rs | 38++++----------------------------------
Msrc/api/web.rs | 8++++----
Msrc/auth.rs | 38+++++++-------------------------------
Msrc/db/models/cipher.rs | 152+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/db/models/collection.rs | 92+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/db/models/device.rs | 12++++++------
Msrc/db/models/org_policy.rs | 24+++++++++++-------------
Msrc/db/models/organization.rs | 212++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/db/models/user.rs | 52+++++++++++++++++++++++++++-------------------------
Msrc/main.rs | 2+-
Msrc/static/global_domains.json | 543+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/static/scripts/bootstrap.bundle.js | 17+++++++++--------
Msrc/static/scripts/bootstrap.css | 133+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/static/scripts/datatables.css | 360+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/static/scripts/datatables.js | 15509+++++++++++++++++++++++++++++++++----------------------------------------------
Asrc/static/scripts/jdenticon-3.3.0.js | 1508+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/static/scripts/jdenticon.js | 1463-------------------------------------------------------------------------------
Dsrc/static/scripts/jquery-3.7.0.slim.js | 8605-------------------------------------------------------------------------------
Asrc/static/scripts/jquery-3.7.1.slim.js | 8617+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util.rs | 116++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
37 files changed, 18807 insertions(+), 20980 deletions(-)

diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs @@ -1,11 +1,12 @@ use crate::{ - api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData}, + api::{EmptyResult, JsonResult, PasswordOrOtpData}, auth::{decode_delete, ClientHeaders, Headers}, db::{ - models::{Cipher, Device, Folder, User, UserKdfType}, + models::{Cipher, Device, Folder, User, UserKdfType, UserOrganization}, DbConn, }, error::Error, + util::NumberOrString, }; use rocket::serde::json::Json; use rocket::{ @@ -22,7 +23,6 @@ pub fn routes() -> Vec<rocket::Route> { get_auth_request_response, get_auth_requests, get_known_device, - get_known_device_from_path, get_public_keys, password_hint, post_auth_request, @@ -55,29 +55,40 @@ pub fn routes() -> Vec<rocket::Route> { ] } -#[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct RegisterData { - Email: String, - Kdf: Option<i32>, - KdfIterations: Option<i32>, - KdfMemory: Option<i32>, - KdfParallelism: Option<i32>, - Key: String, - Keys: Option<KeysData>, - MasterPasswordHash: String, - MasterPasswordHint: Option<String>, - Name: Option<String>, - Token: Option<String>, #[allow(dead_code)] - OrganizationUserId: Option<String>, + email: String, + #[allow(dead_code)] + kdf: Option<i32>, + #[allow(dead_code)] + kdf_iterations: Option<i32>, + #[allow(dead_code)] + kdf_memory: Option<i32>, + #[allow(dead_code)] + kdf_parallelism: Option<i32>, + #[allow(dead_code)] + key: String, + #[allow(dead_code)] + keys: Option<KeysData>, + #[allow(dead_code)] + master_password_hash: String, + #[allow(dead_code)] + master_password_hint: Option<String>, + #[allow(dead_code)] + name: Option<String>, + #[allow(dead_code)] + token: Option<String>, + #[allow(dead_code)] + organization_user_id: Option<String>, } -#[derive(Deserialize)] -#[allow(non_snake_case)] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] struct KeysData { - EncryptedPrivateKey: String, - PublicKey: String, + encrypted_private_key: String, + public_key: String, } /// Trims whitespace from password hints, and converts blank password hints to `None`. @@ -97,7 +108,7 @@ fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/register", data = "<data>")] -fn register(data: JsonUpcase<RegisterData>) -> Error { +fn register(data: Json<RegisterData>) -> Error { const MSG: &str = "Registration is permanently disabled."; Error::new(MSG, MSG) } @@ -107,45 +118,45 @@ async fn profile(headers: Headers, conn: DbConn) -> Json<Value> { } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ProfileData { // Culture: String, // Ignored, always use en-US // MasterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData - Name: String, + name: String, } #[put("/accounts/profile", data = "<data>")] -async fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { +async fn put_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { post_profile(data, headers, conn).await } #[post("/accounts/profile", data = "<data>")] -async fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { - let prof_data: ProfileData = data.into_inner().data; +async fn post_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { + let prof_data: ProfileData = data.into_inner(); // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden) // This also prevents issues with very long usernames causing to large JWT's. See #2419 - if prof_data.Name.len() > 50 { + if prof_data.name.len() > 50 { err!("The field Name must be a string with a maximum length of 50."); } let mut user = headers.user; - user.name = prof_data.Name; + user.name = prof_data.name; user.save(&conn).await?; Ok(Json(user.to_json(&conn).await)) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct AvatarData { - AvatarColor: Option<String>, + avatar_color: Option<String>, } #[put("/accounts/avatar", data = "<data>")] -async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, conn: DbConn) -> JsonResult { - let av_data: AvatarData = data.into_inner().data; +async fn put_avatar(data: Json<AvatarData>, headers: Headers, conn: DbConn) -> JsonResult { + let av_data: AvatarData = data.into_inner(); // It looks like it only supports the 6 hex color format. // If you try to add the short value it will not show that color. // Check and force 7 chars, including the #. - if let Some(ref color) = av_data.AvatarColor { + if let Some(ref color) = av_data.avatar_color { if color.len() != 7 { err!( "The field AvatarColor must be a HTML/Hex color code with a length of 7 characters" @@ -153,7 +164,7 @@ async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, conn: DbConn } } let mut user = headers.user; - user.avatar_color = av_data.AvatarColor; + user.avatar_color = av_data.avatar_color; user.save(&conn).await?; Ok(Json(user.to_json(&conn).await)) } @@ -164,51 +175,47 @@ async fn get_public_keys(uuid: &str, _headers: Headers, conn: DbConn) -> JsonRes err!("User doesn't exist") }; Ok(Json(json!({ - "UserId": user.uuid, - "PublicKey": user.public_key, - "Object":"userKey" + "userId": user.uuid, + "publicKey": user.public_key, + "object":"userKey" }))) } #[post("/accounts/keys", data = "<data>")] -async fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> JsonResult { - let key_data: KeysData = data.into_inner().data; +async fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult { + let key_data: KeysData = data.into_inner(); let mut user = headers.user; - user.private_key = Some(key_data.EncryptedPrivateKey); - user.public_key = Some(key_data.PublicKey); + user.private_key = Some(key_data.encrypted_private_key); + user.public_key = Some(key_data.public_key); user.save(&conn).await?; Ok(Json(json!({ - "PrivateKey": user.private_key, - "PublicKey": user.public_key, - "Object":"keys" + "privateKey": user.private_key, + "publicKey": user.public_key, + "object":"keys" }))) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ChangePassData { - MasterPasswordHash: String, - NewMasterPasswordHash: String, - MasterPasswordHint: Option<String>, - Key: String, + master_password_hash: String, + new_master_password_hash: String, + master_password_hint: Option<String>, + key: String, } #[post("/accounts/password", data = "<data>")] -async fn post_password( - data: JsonUpcase<ChangePassData>, - headers: Headers, - conn: DbConn, -) -> EmptyResult { - let pass_data: ChangePassData = data.into_inner().data; +async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbConn) -> EmptyResult { + let pass_data: ChangePassData = data.into_inner(); let mut user = headers.user; - if !user.check_valid_password(&pass_data.MasterPasswordHash) { + if !user.check_valid_password(&pass_data.master_password_hash) { err!("Invalid password") } - user.password_hint = clean_password_hint(&pass_data.MasterPasswordHint); + user.password_hint = clean_password_hint(&pass_data.master_password_hint); enforce_password_hint_setting(&user.password_hint)?; user.set_password( - &pass_data.NewMasterPasswordHash, - Some(pass_data.Key), + &pass_data.new_master_password_hash, + Some(pass_data.key), true, Some(vec![ String::from("post_rotatekey"), @@ -220,44 +227,44 @@ async fn post_password( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ChangeKdfData { - Kdf: i32, - KdfIterations: u32, - KdfMemory: Option<u32>, - KdfParallelism: Option<u32>, - MasterPasswordHash: String, - NewMasterPasswordHash: String, - Key: String, + kdf: i32, + kdf_iterations: u32, + kdf_memory: Option<u32>, + kdf_parallelism: Option<u32>, + master_password_hash: String, + new_master_password_hash: String, + key: String, } #[post("/accounts/kdf", data = "<data>")] -async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult { - let kdf_data: ChangeKdfData = data.into_inner().data; +async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult { + let kdf_data: ChangeKdfData = data.into_inner(); let mut user = headers.user; - if !user.check_valid_password(&kdf_data.MasterPasswordHash) { + if !user.check_valid_password(&kdf_data.master_password_hash) { err!("Invalid password") } - if kdf_data.Kdf == i32::from(UserKdfType::Pbkdf2) && kdf_data.KdfIterations < 100_000u32 { + if kdf_data.kdf == i32::from(UserKdfType::Pbkdf2) && kdf_data.kdf_iterations < 100_000u32 { err!("PBKDF2 KDF iterations must be at least 100000.") } - if kdf_data.Kdf == i32::from(UserKdfType::Argon2id) { - if kdf_data.KdfIterations < 1u32 { + if kdf_data.kdf == i32::from(UserKdfType::Argon2id) { + if kdf_data.kdf_iterations < 1u32 { err!("Argon2 KDF iterations must be at least 1.") } - if let Some(m) = kdf_data.KdfMemory { + if let Some(m) = kdf_data.kdf_memory { if !(15u32..=1024u32).contains(&m) { err!("Argon2 memory must be between 15 MB and 1024 MB.") } - user.set_client_kdf_memory(kdf_data.KdfMemory); + user.set_client_kdf_memory(kdf_data.kdf_memory); } else { err!("Argon2 memory parameter is required.") } - if let Some(p) = kdf_data.KdfParallelism { + if let Some(p) = kdf_data.kdf_parallelism { if !(1u32..=16u32).contains(&p) { err!("Argon2 parallelism must be between 1 and 16.") } - user.set_client_kdf_parallelism(kdf_data.KdfParallelism); + user.set_client_kdf_parallelism(kdf_data.kdf_parallelism); } else { err!("Argon2 parallelism parameter is required.") } @@ -265,11 +272,11 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbCon user.set_client_kdf_memory(None); user.set_client_kdf_parallelism(None); } - user.set_client_kdf_iter(kdf_data.KdfIterations); - user.client_kdf_type = kdf_data.Kdf; + user.set_client_kdf_iter(kdf_data.kdf_iterations); + user.client_kdf_type = kdf_data.kdf; user.set_password( - &kdf_data.NewMasterPasswordHash, - Some(kdf_data.Key), + &kdf_data.new_master_password_hash, + Some(kdf_data.key), true, None, ); @@ -277,30 +284,38 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbCon } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct UpdateFolderData { - Id: String, - Name: String, + id: Option<String>, + name: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct UpdateResetPasswordData { + organization_id: String, + reset_password_key: String, } use super::ciphers::CipherData; #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct KeyData { - Ciphers: Vec<CipherData>, - Folders: Vec<UpdateFolderData>, - Key: String, - PrivateKey: String, - MasterPasswordHash: String, + ciphers: Vec<CipherData>, + folders: Vec<UpdateFolderData>, + reset_password_keys: Vec<UpdateResetPasswordData>, + key: String, + master_password_hash: String, + private_key: String, } #[post("/accounts/key", data = "<data>")] -async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn) -> EmptyResult { - let key_data: KeyData = data.into_inner().data; +async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn) -> EmptyResult { + let key_data: KeyData = data.into_inner(); if !headers .user - .check_valid_password(&key_data.MasterPasswordHash) + .check_valid_password(&key_data.master_password_hash) { err!("Invalid password") } @@ -308,49 +323,63 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbCon // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_notes(&key_data.Ciphers)?; + Cipher::validate_notes(&key_data.ciphers)?; let user_uuid = &headers.user.uuid; // Update folder data - for folder_data in key_data.Folders { - let Some(mut saved_folder) = Folder::find_by_uuid(&folder_data.Id, &conn).await else { - err!("Folder doesn't exist") - }; - if &saved_folder.user_uuid != user_uuid { - err!("The folder is not owned by the user") + for folder_data in key_data.folders { + if let Some(folder_id) = folder_data.id { + let Some(mut saved_folder) = Folder::find_by_uuid(&folder_id, &conn).await else { + err!("Folder doesn't exist") + }; + if &saved_folder.user_uuid != user_uuid { + err!("The folder is not owned by the user") + } + + saved_folder.name = folder_data.name; + saved_folder.save(&conn).await?; } - saved_folder.name = folder_data.Name; - saved_folder.save(&conn).await?; } - // Update cipher data - use super::ciphers::update_cipher_from_data; - for cipher_data in key_data.Ciphers { - let Some(mut saved_cipher) = - Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &conn).await + for reset_password_data in key_data.reset_password_keys { + let Some(mut user_org) = UserOrganization::find_by_user_and_org( + user_uuid, + &reset_password_data.organization_id, + &conn, + ) + .await else { - err!("Cipher doesn't exist") + err!("Reset password doesn't exist") }; - if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid { - err!("The cipher is not owned by the user") + user_org.reset_password_key = Some(reset_password_data.reset_password_key); + user_org.save(&conn).await?; + } + // Update cipher data + use super::ciphers::update_cipher_from_data; + for cipher_data in key_data.ciphers { + if cipher_data.organization_id.is_none() { + let Some(mut saved_cipher) = + Cipher::find_by_uuid(cipher_data.id.as_ref().unwrap(), &conn).await + else { + err!("Cipher doesn't exist") + }; + if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid { + err!("The cipher is not owned by the user") + } + update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, None, &conn, true) + .await?; } - update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, true) - .await?; } // Update user data let mut user = headers.user; - user.akey = key_data.Key; - user.private_key = Some(key_data.PrivateKey); + user.akey = key_data.key; + user.private_key = Some(key_data.private_key); user.reset_security_stamp(); user.save(&conn).await } #[post("/accounts/security-stamp", data = "<data>")] -async fn post_sstamp( - data: JsonUpcase<PasswordOrOtpData>, - headers: Headers, - conn: DbConn, -) -> EmptyResult { - let otp_data: PasswordOrOtpData = data.into_inner().data; +async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult { + let otp_data: PasswordOrOtpData = data.into_inner(); let mut user = headers.user; otp_data.validate(&user)?; Device::delete_all_by_user(&user.uuid, &conn).await?; @@ -359,32 +388,39 @@ async fn post_sstamp( } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct EmailTokenData { - MasterPasswordHash: String, - NewEmail: String, + #[allow(dead_code)] + master_password_hash: String, + #[allow(dead_code)] + new_email: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/email-token", data = "<data>")] -fn post_email_token(data: JsonUpcase<EmailTokenData>, _headers: Headers) -> Error { +fn post_email_token(data: Json<EmailTokenData>, _headers: Headers) -> Error { const MSG: &str = "E-mail change is not allowed."; Error::new(MSG, MSG) } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct ChangeEmailData { - MasterPasswordHash: String, - NewEmail: String, - Key: String, - NewMasterPasswordHash: String, - Token: NumberOrString, + #[allow(dead_code)] + master_password_hash: String, + #[allow(dead_code)] + new_email: String, + #[allow(dead_code)] + key: String, + #[allow(dead_code)] + new_master_password_hash: String, + #[allow(dead_code)] + token: NumberOrString, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/email", data = "<data>")] -fn post_email(data: JsonUpcase<ChangeEmailData>, _headers: Headers) -> Error { +fn post_email(data: Json<ChangeEmailData>, _headers: Headers) -> Error { const MSG: &str = "E-mail change is not allowed."; Error::new(MSG, MSG) } @@ -397,49 +433,52 @@ fn post_verify_email(_headers: Headers) -> Error { } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct VerifyEmailTokenData { - UserId: String, - Token: String, + #[allow(dead_code)] + user_id: String, + #[allow(dead_code)] + token: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/verify-email-token", data = "<data>")] -fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>) -> Error { +fn post_verify_email_token(data: Json<VerifyEmailTokenData>) -> Error { const MSG: &str = "E-mail is disabled."; Error::new(MSG, MSG) } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct DeleteRecoverData { - Email: String, + #[allow(dead_code)] + email: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/delete-recover", data = "<data>")] -fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>) -> Error { +fn post_delete_recover(data: Json<DeleteRecoverData>) -> Error { const MSG: &str = "Account deletion is disabled with at this endpoint."; Error::new(MSG, MSG) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct DeleteRecoverTokenData { - UserId: String, - Token: String, + user_id: String, + token: String, } #[post("/accounts/delete-recover-token", data = "<data>")] async fn post_delete_recover_token( - data: JsonUpcase<DeleteRecoverTokenData>, + data: Json<DeleteRecoverTokenData>, conn: DbConn, ) -> EmptyResult { - let token_data: DeleteRecoverTokenData = data.into_inner().data; - let Some(user) = User::find_by_uuid(&token_data.UserId, &conn).await else { + let token_data: DeleteRecoverTokenData = data.into_inner(); + let Some(user) = User::find_by_uuid(&token_data.user_id, &conn).await else { err!("User doesn't exist") }; - let Ok(claims) = decode_delete(&token_data.Token) else { + let Ok(claims) = decode_delete(&token_data.token) else { err!("Invalid claim") }; if claims.sub != user.uuid { @@ -450,7 +489,7 @@ async fn post_delete_recover_token( #[post("/accounts/delete", data = "<data>")] async fn post_delete_account( - data: JsonUpcase<PasswordOrOtpData>, + data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -459,11 +498,11 @@ async fn post_delete_account( #[delete("/accounts", data = "<data>")] async fn delete_account( - data: JsonUpcase<PasswordOrOtpData>, + data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, ) -> EmptyResult { - let otp_data: PasswordOrOtpData = data.into_inner().data; + let otp_data: PasswordOrOtpData = data.into_inner(); let user = headers.user; otp_data.validate(&user)?; user.delete(&conn).await @@ -475,33 +514,34 @@ fn revision_date(headers: Headers) -> Json<Value> { } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct PasswordHintData { - Email: String, + #[allow(dead_code)] + email: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/password-hint", data = "<data>")] -fn password_hint(data: JsonUpcase<PasswordHintData>) -> Error { +fn password_hint(data: Json<PasswordHintData>) -> Error { const MSG: &str = "Password hints are disabled."; Error::new(MSG, MSG) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] pub struct PreloginData { - Email: String, + email: String, } #[post("/accounts/prelogin", data = "<data>")] -async fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> { +async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> { _prelogin(data, conn).await } -pub async fn _prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> { - let login_data: PreloginData = data.into_inner().data; +pub async fn _prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> { + let login_data: PreloginData = data.into_inner(); let (kdf_type, kdf_iter, kdf_mem, kdf_para) = - match User::find_by_mail(&login_data.Email, &conn).await { + match User::find_by_mail(&login_data.email, &conn).await { Some(user) => ( user.client_kdf_type, user.client_kdf_iter(), @@ -516,26 +556,26 @@ pub async fn _prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Val ), }; let result = json!({ - "Kdf": kdf_type, - "KdfIterations": kdf_iter, - "KdfMemory": kdf_mem, - "KdfParallelism": kdf_para, + "kdf": kdf_type, + "kdfIterations": kdf_iter, + "kdfMemory": kdf_mem, + "kdfParallelism": kdf_para, }); Json(result) } // https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct SecretVerificationRequest { - MasterPasswordHash: String, + master_password_hash: String, } #[post("/accounts/verify-password", data = "<data>")] -fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers) -> EmptyResult { - let req: SecretVerificationRequest = data.into_inner().data; +fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers) -> EmptyResult { + let req: SecretVerificationRequest = data.into_inner(); let user = headers.user; - if !user.check_valid_password(&req.MasterPasswordHash) { + if !user.check_valid_password(&req.master_password_hash) { err!("Invalid password") } Ok(()) @@ -544,34 +584,27 @@ fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers const API_DISABLED_MSG: &str = "API access is disabled."; #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/api-key", data = "<data>")] -fn api_key(data: JsonUpcase<PasswordOrOtpData>, _headers: Headers) -> Error { +fn api_key(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { Error::new(API_DISABLED_MSG, API_DISABLED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/rotate-api-key", data = "<data>")] -fn rotate_api_key(data: JsonUpcase<PasswordOrOtpData>, _headers: Headers) -> Error { +fn rotate_api_key(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { Error::new(API_DISABLED_MSG, API_DISABLED_MSG) } -// This variant is deprecated: https://github.com/bitwarden/server/pull/2682 -#[get("/devices/knowndevice/<email>/<uuid>")] -async fn get_known_device_from_path(email: &str, uuid: &str, conn: DbConn) -> JsonResult { - // This endpoint doesn't have auth header +#[get("/devices/knowndevice")] +async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult { let mut result = false; - if let Some(user) = User::find_by_mail(email, &conn).await { - result = Device::find_by_uuid_and_user(uuid, &user.uuid, &conn) + if let Some(user) = User::find_by_mail(&device.email, &conn).await { + result = Device::find_by_uuid_and_user(&device.uuid, &user.uuid, &conn) .await .is_some(); } Ok(Json(json!(result))) } -#[get("/devices/knowndevice")] -async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult { - get_known_device_from_path(&device.email, &device.uuid, conn).await -} - struct KnownDevice { email: String, uuid: String, @@ -612,15 +645,18 @@ impl<'r> FromRequest<'r> for KnownDevice { } #[derive(Deserialize)] -#[allow(non_snake_case)] -struct PushToken; +#[serde(rename_all = "camelCase")] +struct PushToken { + #[allow(dead_code)] + push_token: String, +} #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/devices/identifier/<uuid>/token", data = "<data>")] -fn post_device_token(uuid: &str, data: JsonUpcase<PushToken>, _headers: Headers) {} +fn post_device_token(uuid: &str, data: Json<PushToken>, _headers: Headers) {} #[allow(unused_variables, clippy::needless_pass_by_value)] #[put("/devices/identifier/<uuid>/token", data = "<data>")] -fn put_device_token(uuid: &str, data: JsonUpcase<PushToken>, _headers: Headers) {} +fn put_device_token(uuid: &str, data: Json<PushToken>, _headers: Headers) {} #[allow(unused_variables)] #[put("/devices/identifier/<uuid>/clear-token")] const fn put_clear_device_token(uuid: &str) {} @@ -631,12 +667,16 @@ const fn put_clear_device_token(uuid: &str) {} const fn post_clear_device_token(uuid: &str) {} #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct AuthRequestRequest { - accessCode: String, - deviceIdentifier: String, + #[allow(dead_code)] + access_code: String, + #[allow(dead_code)] + device_identifier: String, + #[allow(dead_code)] email: String, - publicKey: String, + #[allow(dead_code)] + public_key: String, #[serde(alias = "type")] _type: i32, } @@ -654,12 +694,16 @@ fn get_auth_request(uuid: &str) -> Error { } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct AuthResponseRequest { - deviceIdentifier: String, + #[allow(dead_code)] + device_identifier: String, + #[allow(dead_code)] key: String, - masterPasswordHash: Option<String>, - requestApproved: bool, + #[allow(dead_code)] + master_password_hash: Option<String>, + #[allow(dead_code)] + request_approved: bool, } #[allow(unused_variables, clippy::needless_pass_by_value)] diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -1,6 +1,6 @@ use super::folders::FolderData; use crate::{ - api::{self, EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData}, + api::{self, EmptyResult, JsonResult, PasswordOrOtpData}, auth::Headers, db::{ models::{ @@ -10,12 +10,13 @@ use crate::{ DbConn, }, error::Error, + util::NumberOrString, }; use chrono::{NaiveDateTime, Utc}; -use rocket::fs::TempFile; -use rocket::serde::json::Json; use rocket::{ form::{Form, FromForm}, + fs::TempFile, + serde::json::Json, Route, }; use serde_json::Value; @@ -69,6 +70,7 @@ pub fn routes() -> Vec<Route> { post_ciphers_admin, post_ciphers_create, post_ciphers_import, + post_collections2_update, post_collections_admin, post_collections_update, put_cipher, @@ -76,6 +78,7 @@ pub fn routes() -> Vec<Route> { put_cipher_partial, put_cipher_share, put_cipher_share_selected, + put_collections2_update, put_collections_admin, put_collections_update, restore_cipher_put, @@ -136,15 +139,15 @@ async fn sync(data: SyncData, headers: Headers, conn: DbConn) -> Json<Value> { api::core::_get_eq_domains(headers, true).into_inner() }; Json(json!({ - "Profile": user_json, - "Folders": folders_json, - "Collections": collections_json, - "Policies": policies_json, - "Ciphers": ciphers_json, - "Domains": domains_json, - "Sends": Vec::<Value>::new(), + "profile": user_json, + "folders": folders_json, + "collections": collections_json, + "policies": policies_json, + "ciphers": ciphers_json, + "domains": domains_json, + "sends": Vec::<Value>::new(), "unofficialServer": true, - "Object": "sync" + "object": "sync" })) } @@ -166,9 +169,9 @@ async fn get_ciphers(headers: Headers, conn: DbConn) -> Json<Value> { ); } Json(json!({ - "Data": ciphers_json, - "Object": "list", - "ContinuationToken": null + "data": ciphers_json, + "object": "list", + "continuationToken": null })) } @@ -202,59 +205,63 @@ async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonR } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] pub struct CipherData { - // Id is optional as it is included only in bulk share - pub Id: Option<String>, - // Folder id is not included in import - FolderId: Option<String>, + // id is optional as it is included only in bulk share + pub id: Option<String>, + // folder id is not included in import + folder_id: Option<String>, // TODO: Some of these might appear all the time, no need for Option - OrganizationId: Option<String>, - Key: Option<String>, - pub Type: i32, - pub Name: String, - pub Notes: Option<String>, - Fields: Option<Value>, + #[serde(alias = "organizationID")] + pub organization_id: Option<String>, + key: Option<String>, + pub r#type: i32, + pub name: String, + pub notes: Option<String>, + fields: Option<Value>, // Only one of these should exist, depending on type - Login: Option<Value>, - SecureNote: Option<Value>, - Card: Option<Value>, - Identity: Option<Value>, - Favorite: Option<bool>, - Reprompt: Option<i32>, - PasswordHistory: Option<Value>, + login: Option<Value>, + secure_note: Option<Value>, + card: Option<Value>, + identity: Option<Value>, + favorite: Option<bool>, + reprompt: Option<i32>, + password_history: Option<Value>, // These are used during key rotation // 'Attachments' is unused, contains map of {id: filename} - #[serde(rename = "Attachments")] - _Attachments: Option<Value>, - Attachments2: Option<HashMap<String, Attachments2Data>>, + #[allow(dead_code)] + attachments: Option<Value>, + #[allow(dead_code)] + attachments2: Option<HashMap<String, Attachments2Data>>, // The revision datetime (in ISO 8601 format) of the client's local copy // of the cipher. This is used to prevent a client from updating a cipher // when it doesn't have the latest version, as that can result in data // loss. It's not an error when no value is provided; this can happen // when using older client versions, or if the operation doesn't involve // updating an existing cipher. - LastKnownRevisionDate: Option<String>, + last_known_revision_date: Option<String>, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct PartialCipherData { - FolderId: Option<String>, - Favorite: bool, + folder_id: Option<String>, + favorite: bool, } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct Attachments2Data { - FileName: String, - Key: String, + #[allow(dead_code)] + file_name: String, + #[allow(dead_code)] + key: String, } /// Called when an org admin clones an org cipher. #[post("/ciphers/admin", data = "<data>")] async fn post_ciphers_admin( - data: JsonUpcase<ShareCipherData>, + data: Json<ShareCipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { @@ -266,21 +273,25 @@ async fn post_ciphers_admin( /// `organizationId` is null. #[post("/ciphers/create", data = "<data>")] async fn post_ciphers_create( - data: JsonUpcase<ShareCipherData>, + data: Json<ShareCipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let mut data: ShareCipherData = data.into_inner().data; + let mut data: ShareCipherData = data.into_inner(); // Check if there are one more more collections selected when this cipher is part of an organization. // err if this is not the case before creating an empty cipher. - if data.Cipher.OrganizationId.is_some() && data.CollectionIds.is_empty() { + if data.cipher.organization_id.is_some() && data.collection_ids.is_empty() { err!("You must select at least one collection."); } + // reverse sanity check to prevent corruptions + if !data.collection_ids.is_empty() && data.cipher.organization_id.is_none() { + err!("The client has not provided an organization id!"); + } // This check is usually only needed in update_cipher_from_data(), but we // need it here as well to avoid creating an empty cipher in the call to // cipher.save() below. - enforce_personal_ownership_policy(Some(&data.Cipher), &headers, &conn).await?; - let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone()); + enforce_personal_ownership_policy(Some(&data.cipher), &headers, &conn).await?; + let mut cipher = Cipher::new(data.cipher.r#type, data.cipher.name.clone()); cipher.user_uuid = Some(headers.user.uuid.clone()); cipher.save(&conn).await?; // When cloning a cipher, the Bitwarden clients seem to set this field @@ -289,21 +300,21 @@ async fn post_ciphers_create( // the current time, so the stale data check will end up failing down the // line. Since this function only creates new ciphers (whether by cloning // or otherwise), we can just ignore this field entirely. - data.Cipher.LastKnownRevisionDate = None; + data.cipher.last_known_revision_date = None; share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn).await } /// Called when creating a new user-owned cipher. #[post("/ciphers", data = "<data>")] -async fn post_ciphers(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn) -> JsonResult { - let mut data: CipherData = data.into_inner().data; +async fn post_ciphers(data: Json<CipherData>, headers: Headers, conn: DbConn) -> JsonResult { + let mut data: CipherData = data.into_inner(); // The web/browser clients set this field to null as expected, but the // mobile clients seem to set the invalid value `0001-01-01T00:00:00`, // which results in a warning message being logged. This field isn't // needed when creating a new cipher, so just ignore it unconditionally. - data.LastKnownRevisionDate = None; - let mut cipher = Cipher::new(data.Type, data.Name.clone()); - update_cipher_from_data(&mut cipher, data, &headers, false, &conn, false).await?; + data.last_known_revision_date = None; + let mut cipher = Cipher::new(data.r#type, data.name.clone()); + update_cipher_from_data(&mut cipher, data, &headers, None, &conn, false).await?; Ok(Json( cipher .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn) @@ -323,7 +334,7 @@ async fn enforce_personal_ownership_policy( headers: &Headers, conn: &DbConn, ) -> EmptyResult { - if data.is_none() || data.unwrap().OrganizationId.is_none() { + if data.is_none() || data.unwrap().organization_id.is_none() { let user_uuid = &headers.user.uuid; let policy_type = OrgPolicyType::PersonalOwnership; if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, None, conn).await { @@ -337,7 +348,7 @@ pub async fn update_cipher_from_data( cipher: &mut Cipher, data: CipherData, headers: &Headers, - shared_to_collection: bool, + shared_to_collections: Option<Vec<String>>, conn: &DbConn, import_cipher: bool, ) -> EmptyResult { @@ -345,7 +356,7 @@ pub async fn update_cipher_from_data( // Check that the client isn't updating an existing cipher with stale data. // And only perform this check when not importing ciphers, else the date/time check will fail. if !import_cipher { - if let Some(dt) = data.LastKnownRevisionDate { + if let Some(dt) = data.last_known_revision_date { match NaiveDateTime::parse_from_str(&dt, "%+") { // ISO 8601 format Err(err) => warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err), @@ -356,19 +367,19 @@ pub async fn update_cipher_from_data( } } } - if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.OrganizationId { + if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.organization_id { err!("Organization mismatch. Please resync the client before updating the cipher") } - if let Some(ref note) = data.Notes { + if let Some(ref note) = data.notes { if note.len() > 10_000 { err!("The field Notes exceeds the maximum encrypted value length of 10000 characters.") } } - if let Some(org_id) = data.OrganizationId { + if let Some(org_id) = data.organization_id { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await { None => err!("You don't have permission to add item to organization"), Some(org_user) => { - if shared_to_collection + if shared_to_collections.is_some() || org_user.has_full_access() || cipher .is_write_accessible_to_user(&headers.user.uuid, conn) @@ -389,7 +400,7 @@ pub async fn update_cipher_from_data( } else { cipher.user_uuid = Some(headers.user.uuid.clone()); } - if let Some(ref folder_id) = data.FolderId { + if let Some(ref folder_id) = data.folder_id { match Folder::find_by_uuid(folder_id, conn).await { Some(folder) => { if folder.user_uuid != headers.user.uuid { @@ -416,11 +427,11 @@ pub async fn update_cipher_from_data( }; json_data } - let type_data_opt = match data.Type { - 1i32 => data.Login, - 2i32 => data.SecureNote, - 3i32 => data.Card, - 4i32 => data.Identity, + let type_data_opt = match data.r#type { + 1i32 => data.login, + 2i32 => data.secure_note, + 3i32 => data.card, + 4i32 => data.identity, _ => err!("Invalid type"), }; let type_data = match type_data_opt { @@ -435,71 +446,81 @@ pub async fn update_cipher_from_data( } None => err!("Data missing"), }; - cipher.key = data.Key; - cipher.name = data.Name; - cipher.notes = data.Notes; - cipher.fields = data.Fields.map(|f| _clean_cipher_data(f).to_string()); + cipher.key = data.key; + cipher.name = data.name; + cipher.notes = data.notes; + cipher.fields = data.fields.map(|f| _clean_cipher_data(f).to_string()); cipher.data = type_data.to_string(); - cipher.password_history = data.PasswordHistory.map(|f| f.to_string()); - cipher.reprompt = data.Reprompt; + cipher.password_history = data.password_history.map(|f| f.to_string()); + cipher.reprompt = data.reprompt; cipher.save(conn).await?; cipher - .move_to_folder(data.FolderId, &headers.user.uuid, conn) + .move_to_folder(data.folder_id, &headers.user.uuid, conn) .await?; cipher - .set_favorite(data.Favorite, &headers.user.uuid, conn) + .set_favorite(data.favorite, &headers.user.uuid, conn) .await?; Ok(()) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ImportData { - Ciphers: Vec<CipherData>, - Folders: Vec<FolderData>, - FolderRelationships: Vec<RelationsData>, + ciphers: Vec<CipherData>, + folders: Vec<FolderData>, + folder_relationships: Vec<RelationsData>, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct RelationsData { // Cipher id - Key: usize, + key: usize, // Folder id - Value: usize, + value: usize, } #[post("/ciphers/import", data = "<data>")] async fn post_ciphers_import( - data: JsonUpcase<ImportData>, + data: Json<ImportData>, headers: Headers, conn: DbConn, ) -> EmptyResult { enforce_personal_ownership_policy(None, &headers, &conn).await?; - let data: ImportData = data.into_inner().data; + let data: ImportData = data.into_inner(); // Validate the import before continuing // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_notes(&data.Ciphers)?; - // Read and create the folders - let mut folders: Vec<_> = Vec::new(); - for folder in data.Folders { - let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.Name); - new_folder.save(&conn).await?; - folders.push(new_folder); + Cipher::validate_notes(&data.ciphers)?; + let existing_folders: Vec<String> = Folder::find_by_user(&headers.user.uuid, &conn) + .await + .into_iter() + .map(|f| f.uuid) + .collect(); + let mut folders: Vec<String> = Vec::with_capacity(data.folders.len()); + for folder in data.folders { + let folder_uuid = + if folder.id.is_some() && existing_folders.contains(folder.id.as_ref().unwrap()) { + folder.id.unwrap() + } else { + let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name); + new_folder.save(&conn).await?; + new_folder.uuid + }; + folders.push(folder_uuid); } // Read the relations between folders and ciphers - let mut relations_map = HashMap::new(); - for relation in data.FolderRelationships { - relations_map.insert(relation.Key, relation.Value); + let mut relations_map = HashMap::with_capacity(data.folder_relationships.len()); + for relation in data.folder_relationships { + relations_map.insert(relation.key, relation.value); } // Read and create the ciphers - for (index, mut cipher_data) in data.Ciphers.into_iter().enumerate() { - let folder_uuid = relations_map.get(&index).map(|i| folders[*i].uuid.clone()); - cipher_data.FolderId = folder_uuid; - let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); - update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, true).await?; + for (index, mut cipher_data) in data.ciphers.into_iter().enumerate() { + let folder_uuid = relations_map.get(&index).map(|i| folders[*i].clone()); + cipher_data.folder_id = folder_uuid; + let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone()); + update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &conn, true).await?; } let mut user = headers.user; user.update_revision(&conn).await?; @@ -510,7 +531,7 @@ async fn post_ciphers_import( #[put("/ciphers/<uuid>/admin", data = "<data>")] async fn put_cipher_admin( uuid: &str, - data: JsonUpcase<CipherData>, + data: Json<CipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { @@ -520,7 +541,7 @@ async fn put_cipher_admin( #[post("/ciphers/<uuid>/admin", data = "<data>")] async fn post_cipher_admin( uuid: &str, - data: JsonUpcase<CipherData>, + data: Json<CipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { @@ -530,7 +551,7 @@ async fn post_cipher_admin( #[post("/ciphers/<uuid>", data = "<data>")] async fn post_cipher( uuid: &str, - data: JsonUpcase<CipherData>, + data: Json<CipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { @@ -540,11 +561,11 @@ async fn post_cipher( #[put("/ciphers/<uuid>", data = "<data>")] async fn put_cipher( uuid: &str, - data: JsonUpcase<CipherData>, + data: Json<CipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: CipherData = data.into_inner().data; + let data: CipherData = data.into_inner(); let Some(mut cipher) = Cipher::find_by_uuid(uuid, &conn).await else { err!("Cipher doesn't exist") }; @@ -558,7 +579,7 @@ async fn put_cipher( { err!("Cipher is not write accessible") } - update_cipher_from_data(&mut cipher, data, &headers, false, &conn, false).await?; + update_cipher_from_data(&mut cipher, data, &headers, None, &conn, false).await?; Ok(Json( cipher .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn) @@ -569,7 +590,7 @@ async fn put_cipher( #[post("/ciphers/<uuid>/partial", data = "<data>")] async fn post_cipher_partial( uuid: &str, - data: JsonUpcase<PartialCipherData>, + data: Json<PartialCipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { @@ -580,15 +601,15 @@ async fn post_cipher_partial( #[put("/ciphers/<uuid>/partial", data = "<data>")] async fn put_cipher_partial( uuid: &str, - data: JsonUpcase<PartialCipherData>, + data: Json<PartialCipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: PartialCipherData = data.into_inner().data; + let data: PartialCipherData = data.into_inner(); let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else { err!("Cipher doesn't exist") }; - if let Some(ref folder_id) = data.FolderId { + if let Some(ref folder_id) = data.folder_id { match Folder::find_by_uuid(folder_id, &conn).await { Some(folder) => { if folder.user_uuid != headers.user.uuid { @@ -600,11 +621,11 @@ async fn put_cipher_partial( } // Move cipher cipher - .move_to_folder(data.FolderId.clone(), &headers.user.uuid, &conn) + .move_to_folder(data.folder_id.clone(), &headers.user.uuid, &conn) .await?; // Update favorite cipher - .set_favorite(Some(data.Favorite), &headers.user.uuid, &conn) + .set_favorite(Some(data.favorite), &headers.user.uuid, &conn) .await?; Ok(Json( cipher @@ -614,35 +635,103 @@ async fn put_cipher_partial( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct CollectionsAdminData { - CollectionIds: Vec<String>, + collection_ids: Vec<String>, +} + +#[put("/ciphers/<uuid>/collections_v2", data = "<data>")] +async fn put_collections2_update( + uuid: &str, + data: Json<CollectionsAdminData>, + headers: Headers, + conn: DbConn, +) -> JsonResult { + post_collections2_update(uuid, data, headers, conn).await +} + +#[post("/ciphers/<uuid>/collections_v2", data = "<data>")] +async fn post_collections2_update( + uuid: &str, + data: Json<CollectionsAdminData>, + headers: Headers, + conn: DbConn, +) -> JsonResult { + let cipher_details = post_collections_update(uuid, data, headers, conn).await?; + Ok(Json(json!({ + "object": "optionalCipherDetails", + "unavailable": false, + "cipher": *cipher_details + }))) } #[put("/ciphers/<uuid>/collections", data = "<data>")] async fn put_collections_update( uuid: &str, - data: JsonUpcase<CollectionsAdminData>, + data: Json<CollectionsAdminData>, headers: Headers, conn: DbConn, -) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn).await +) -> JsonResult { + post_collections_update(uuid, data, headers, conn).await } #[post("/ciphers/<uuid>/collections", data = "<data>")] async fn post_collections_update( uuid: &str, - data: JsonUpcase<CollectionsAdminData>, + data: Json<CollectionsAdminData>, headers: Headers, conn: DbConn, -) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn).await +) -> JsonResult { + let data: CollectionsAdminData = data.into_inner(); + let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else { + err!("Cipher doesn't exist") + }; + if !cipher + .is_write_accessible_to_user(&headers.user.uuid, &conn) + .await + { + err!("Cipher is not write accessible") + } + + let posted_collections = HashSet::<String>::from_iter(data.collection_ids); + let current_collections = HashSet::<String>::from_iter( + cipher + .get_collections(headers.user.uuid.clone(), &conn) + .await, + ); + + for collection in posted_collections.symmetric_difference(&current_collections) { + match Collection::find_by_uuid(collection, &conn).await { + None => err!("Invalid collection ID provided"), + Some(collection) => { + if collection + .is_writable_by_user(&headers.user.uuid, &conn) + .await + { + if posted_collections.contains(&collection.uuid) { + // Add to collection + CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn).await?; + } else { + // Remove from collection + CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn).await?; + } + } else { + err!("No rights to modify the collection") + } + } + } + } + Ok(Json( + cipher + .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn) + .await, + )) } #[put("/ciphers/<uuid>/collections-admin", data = "<data>")] async fn put_collections_admin( uuid: &str, - data: JsonUpcase<CollectionsAdminData>, + data: Json<CollectionsAdminData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -652,11 +741,11 @@ async fn put_collections_admin( #[post("/ciphers/<uuid>/collections-admin", data = "<data>")] async fn post_collections_admin( uuid: &str, - data: JsonUpcase<CollectionsAdminData>, + data: Json<CollectionsAdminData>, headers: Headers, conn: DbConn, ) -> EmptyResult { - let data: CollectionsAdminData = data.into_inner().data; + let data: CollectionsAdminData = data.into_inner(); let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else { err!("Cipher doesn't exist") }; @@ -666,14 +755,12 @@ async fn post_collections_admin( { err!("Cipher is not write accessible") } - - let posted_collections: HashSet<String> = data.CollectionIds.iter().cloned().collect(); - let current_collections: HashSet<String> = cipher - .get_collections(headers.user.uuid.clone(), &conn) - .await - .iter() - .cloned() - .collect(); + let posted_collections = HashSet::<String>::from_iter(data.collection_ids); + let current_collections = HashSet::<String>::from_iter( + cipher + .get_admin_collections(headers.user.uuid.clone(), &conn) + .await, + ); for collection in posted_collections.symmetric_difference(&current_collections) { match Collection::find_by_uuid(collection, &conn).await { None => err!("Invalid collection ID provided"), @@ -699,65 +786,67 @@ async fn post_collections_admin( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ShareCipherData { - Cipher: CipherData, - CollectionIds: Vec<String>, + #[serde(alias = "Cipher")] + cipher: CipherData, + #[serde(alias = "CollectionIds")] + collection_ids: Vec<String>, } #[post("/ciphers/<uuid>/share", data = "<data>")] async fn post_cipher_share( uuid: &str, - data: JsonUpcase<ShareCipherData>, + data: Json<ShareCipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: ShareCipherData = data.into_inner().data; + let data: ShareCipherData = data.into_inner(); share_cipher_by_uuid(uuid, data, &headers, &conn).await } #[put("/ciphers/<uuid>/share", data = "<data>")] async fn put_cipher_share( uuid: &str, - data: JsonUpcase<ShareCipherData>, + data: Json<ShareCipherData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: ShareCipherData = data.into_inner().data; + let data: ShareCipherData = data.into_inner(); share_cipher_by_uuid(uuid, data, &headers, &conn).await } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ShareSelectedCipherData { - Ciphers: Vec<CipherData>, - CollectionIds: Vec<String>, + ciphers: Vec<CipherData>, + collection_ids: Vec<String>, } #[put("/ciphers/share", data = "<data>")] async fn put_cipher_share_selected( - data: JsonUpcase<ShareSelectedCipherData>, + data: Json<ShareSelectedCipherData>, headers: Headers, conn: DbConn, ) -> EmptyResult { - let mut data: ShareSelectedCipherData = data.into_inner().data; - if data.Ciphers.is_empty() { + let mut data: ShareSelectedCipherData = data.into_inner(); + if data.ciphers.is_empty() { err!("You must select at least one cipher.") } - if data.CollectionIds.is_empty() { + if data.collection_ids.is_empty() { err!("You must select at least one collection.") } - for cipher in &data.Ciphers { - if cipher.Id.is_none() { + for cipher in &data.ciphers { + if cipher.id.is_none() { err!("Request missing ids field"); } } - while let Some(cipher) = data.Ciphers.pop() { + while let Some(cipher) = data.ciphers.pop() { let mut shared_cipher_data = ShareCipherData { - Cipher: cipher, - CollectionIds: data.CollectionIds.clone(), + cipher, + collection_ids: data.collection_ids.clone(), }; - match shared_cipher_data.Cipher.Id.take() { + match shared_cipher_data.cipher.id.take() { Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn).await?, None => err!("Request missing ids field"), }; @@ -784,9 +873,9 @@ async fn share_cipher_by_uuid( } None => err!("Cipher doesn't exist"), }; - let mut shared_to_collection = false; - if let Some(ref organization_uuid) = data.Cipher.OrganizationId { - for col_uuid in &data.CollectionIds { + let mut shared_to_collections = Vec::new(); + if let Some(ref organization_uuid) = data.cipher.organization_id { + for col_uuid in &data.collection_ids { match Collection::find_by_uuid_and_org(col_uuid, organization_uuid, conn).await { None => err!("Invalid collection ID provided"), Some(collection) => { @@ -795,7 +884,7 @@ async fn share_cipher_by_uuid( .await { CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?; - shared_to_collection = true; + shared_to_collections.push(collection.uuid); } else { err!("No rights to modify the collection") } @@ -805,9 +894,9 @@ async fn share_cipher_by_uuid( }; update_cipher_from_data( &mut cipher, - data.Cipher, + data.cipher, headers, - shared_to_collection, + Some(shared_to_collections), conn, false, ) @@ -833,12 +922,16 @@ fn get_attachment(uuid: &str, attachment_id: &str, _headers: Headers) -> Error { } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct AttachmentRequestData { - Key: String, - FileName: String, - FileSize: i32, - AdminRequest: Option<bool>, // true when attaching from an org vault view + #[allow(dead_code)] + key: String, + #[allow(dead_code)] + file_name: String, + #[allow(dead_code)] + file_size: NumberOrString, + #[allow(dead_code)] + admin_request: Option<bool>, // true when attaching from an org vault view } /// v2 API for creating an attachment associated with a cipher. @@ -847,11 +940,7 @@ struct AttachmentRequestData { /// For self-hosted instances, it's another API on the local instance. #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/ciphers/<uuid>/attachment/v2", data = "<data>")] -fn post_attachment_v2( - uuid: &str, - data: JsonUpcase<AttachmentRequestData>, - _headers: Headers, -) -> Error { +fn post_attachment_v2(uuid: &str, data: Json<AttachmentRequestData>, _headers: Headers) -> Error { Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG) } @@ -974,7 +1063,7 @@ async fn delete_cipher_admin(uuid: &str, headers: Headers, conn: DbConn) -> Empt #[delete("/ciphers", data = "<data>")] async fn delete_cipher_selected( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -983,7 +1072,7 @@ async fn delete_cipher_selected( #[post("/ciphers/delete", data = "<data>")] async fn delete_cipher_selected_post( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -992,7 +1081,7 @@ async fn delete_cipher_selected_post( #[put("/ciphers/delete", data = "<data>")] async fn delete_cipher_selected_put( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -1001,7 +1090,7 @@ async fn delete_cipher_selected_put( #[delete("/ciphers/admin", data = "<data>")] async fn delete_cipher_selected_admin( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -1010,7 +1099,7 @@ async fn delete_cipher_selected_admin( #[post("/ciphers/delete-admin", data = "<data>")] async fn delete_cipher_selected_post_admin( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -1019,7 +1108,7 @@ async fn delete_cipher_selected_post_admin( #[put("/ciphers/delete-admin", data = "<data>")] async fn delete_cipher_selected_put_admin( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -1038,7 +1127,7 @@ async fn restore_cipher_put_admin(uuid: &str, headers: Headers, conn: DbConn) -> #[put("/ciphers/restore", data = "<data>")] async fn restore_cipher_selected( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, ) -> JsonResult { @@ -1046,21 +1135,21 @@ async fn restore_cipher_selected( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct MoveCipherData { - FolderId: Option<String>, - Ids: Vec<String>, + folder_id: Option<String>, + ids: Vec<String>, } #[post("/ciphers/move", data = "<data>")] async fn move_cipher_selected( - data: JsonUpcase<MoveCipherData>, + data: Json<MoveCipherData>, headers: Headers, conn: DbConn, ) -> EmptyResult { - let data = data.into_inner().data; + let data = data.into_inner(); let user_uuid = headers.user.uuid; - if let Some(ref folder_id) = data.FolderId { + if let Some(ref folder_id) = data.folder_id { match Folder::find_by_uuid(folder_id, &conn).await { Some(folder) => { if folder.user_uuid != user_uuid { @@ -1070,7 +1159,7 @@ async fn move_cipher_selected( None => err!("Folder doesn't exist"), } } - for uuid in data.Ids { + for uuid in data.ids { let Some(cipher) = Cipher::find_by_uuid(&uuid, &conn).await else { err!("Cipher doesn't exist") }; @@ -1079,7 +1168,7 @@ async fn move_cipher_selected( } // Move cipher cipher - .move_to_folder(data.FolderId.clone(), &user_uuid, &conn) + .move_to_folder(data.folder_id.clone(), &user_uuid, &conn) .await?; } Ok(()) @@ -1087,7 +1176,7 @@ async fn move_cipher_selected( #[put("/ciphers/move", data = "<data>")] async fn move_cipher_selected_put( - data: JsonUpcase<MoveCipherData>, + data: Json<MoveCipherData>, headers: Headers, conn: DbConn, ) -> EmptyResult { @@ -1103,11 +1192,11 @@ struct OrganizationId { #[post("/ciphers/purge?<organization..>", data = "<data>")] async fn delete_all( organization: Option<OrganizationId>, - data: JsonUpcase<PasswordOrOtpData>, + data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, ) -> EmptyResult { - let data: PasswordOrOtpData = data.into_inner().data; + let data: PasswordOrOtpData = data.into_inner(); let mut user = headers.user; data.validate(&user)?; if let Some(org_data) = organization { @@ -1162,22 +1251,21 @@ async fn _delete_cipher_by_uuid( Ok(()) } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct CipherIdsData { + ids: Vec<String>, +} + async fn _delete_multiple_ciphers( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: Headers, conn: DbConn, soft_delete: bool, ) -> EmptyResult { - let data: Value = data.into_inner().data; - let uuids = match data.get("Ids") { - Some(ids) => match ids.as_array() { - Some(ids) => ids.iter().filter_map(Value::as_str), - None => err!("Posted ids field is not an array"), - }, - None => err!("Request missing ids field"), - }; - for uuid in uuids { - if let error @ Err(_) = _delete_cipher_by_uuid(uuid, &headers, &conn, soft_delete).await { + let data = data.into_inner(); + for uuid in data.ids { + if let error @ Err(_) = _delete_cipher_by_uuid(&uuid, &headers, &conn, soft_delete).await { return error; }; } @@ -1204,29 +1292,22 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn) - } async fn _restore_multiple_ciphers( - data: JsonUpcase<Value>, + data: Json<CipherIdsData>, headers: &Headers, conn: &DbConn, ) -> JsonResult { - let data: Value = data.into_inner().data; - let uuids = match data.get("Ids") { - Some(ids) => match ids.as_array() { - Some(ids) => ids.iter().filter_map(Value::as_str), - None => err!("Posted ids field is not an array"), - }, - None => err!("Request missing ids field"), - }; + let data = data.into_inner(); let mut ciphers: Vec<Value> = Vec::new(); - for uuid in uuids { - match _restore_cipher_by_uuid(uuid, headers, conn).await { + for uuid in data.ids { + match _restore_cipher_by_uuid(&uuid, headers, conn).await { Ok(json) => ciphers.push(json.into_inner()), err => return err, } } Ok(Json(json!({ - "Data": ciphers, - "Object": "list", - "ContinuationToken": null + "data": ciphers, + "object": "list", + "continuationToken": null }))) } /// This will hold all the necessary data to improve a full sync of all the ciphers diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs @@ -1,8 +1,4 @@ -use crate::{ - api::{JsonUpcase, NumberOrString}, - auth::Headers, - error::Error, -}; +use crate::{auth::Headers, error::Error, util::NumberOrString}; use rocket::{serde::json::Json, Route}; use serde_json::Value; @@ -32,43 +28,54 @@ pub fn routes() -> Vec<Route> { #[get("/emergency-access/trusted")] fn get_contacts(_headers: Headers) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": null + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": null })) } #[allow(clippy::needless_pass_by_value)] #[get("/emergency-access/granted")] fn get_grantees(_headers: Headers) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": null + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": null })) } const ACCESS_NOT_ALLOWED_MSG: &str = "Emergency access is not allowed."; -#[allow(unused_variables)] +#[allow(unused_variables, clippy::needless_pass_by_value)] #[get("/emergency-access/<emer_id>")] -fn get_emergency_access(emer_id: &str) -> Error { +fn get_emergency_access(emer_id: &str, _headers: Headers) -> Error { Error::new(ACCESS_NOT_ALLOWED_MSG, ACCESS_NOT_ALLOWED_MSG) } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct EmergencyAccessUpdateData { - Type: NumberOrString, - WaitTimeDays: i32, - KeyEncrypted: Option<String>, + #[allow(dead_code)] + r#type: NumberOrString, + #[allow(dead_code)] + wait_time_days: i32, + #[allow(dead_code)] + key_encrypted: Option<String>, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[put("/emergency-access/<emer_id>", data = "<data>")] -fn put_emergency_access(emer_id: &str, data: JsonUpcase<EmergencyAccessUpdateData>) -> Error { +fn put_emergency_access( + emer_id: &str, + data: Json<EmergencyAccessUpdateData>, + _headers: Headers, +) -> Error { Error::new(ACCESS_NOT_ALLOWED_MSG, ACCESS_NOT_ALLOWED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/emergency-access/<emer_id>", data = "<data>")] -fn post_emergency_access(emer_id: &str, data: JsonUpcase<EmergencyAccessUpdateData>) -> Error { +fn post_emergency_access( + emer_id: &str, + data: Json<EmergencyAccessUpdateData>, + _headers: Headers, +) -> Error { Error::new(ACCESS_NOT_ALLOWED_MSG, ACCESS_NOT_ALLOWED_MSG) } @@ -85,16 +92,19 @@ fn post_delete_emergency_access(emer_id: &str, _headers: Headers) -> Error { } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct EmergencyAccessInviteData { - Email: String, - Type: NumberOrString, - WaitTimeDays: i32, + #[allow(dead_code)] + email: String, + #[allow(dead_code)] + r#type: NumberOrString, + #[allow(dead_code)] + wait_time_days: i32, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/emergency-access/invite", data = "<data>")] -fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, _headers: Headers) -> Error { +fn send_invite(data: Json<EmergencyAccessInviteData>, _headers: Headers) -> Error { Error::new(ACCESS_NOT_ALLOWED_MSG, ACCESS_NOT_ALLOWED_MSG) } @@ -105,30 +115,28 @@ fn resend_invite(emer_id: &str, _headers: Headers) -> Error { } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct AcceptData { - Token: String, + #[allow(dead_code)] + token: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/emergency-access/<emer_id>/accept", data = "<data>")] -fn accept_invite(emer_id: &str, data: JsonUpcase<AcceptData>, _headers: Headers) -> Error { +fn accept_invite(emer_id: &str, data: Json<AcceptData>, _headers: Headers) -> Error { Error::new(ACCESS_NOT_ALLOWED_MSG, ACCESS_NOT_ALLOWED_MSG) } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct ConfirmData { - Key: String, + #[allow(dead_code)] + key: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/emergency-access/<emer_id>/confirm", data = "<data>")] -fn confirm_emergency_access( - emer_id: &str, - data: JsonUpcase<ConfirmData>, - _headers: Headers, -) -> Error { +fn confirm_emergency_access(emer_id: &str, data: Json<ConfirmData>, _headers: Headers) -> Error { Error::new(ACCESS_NOT_ALLOWED_MSG, ACCESS_NOT_ALLOWED_MSG) } @@ -163,17 +171,19 @@ fn takeover_emergency_access(emer_id: &str, _headers: Headers) -> Error { } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct EmergencyAccessPasswordData { - NewMasterPasswordHash: String, - Key: String, + #[allow(dead_code)] + new_master_password_hash: String, + #[allow(dead_code)] + key: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/emergency-access/<emer_id>/password", data = "<data>")] fn password_emergency_access( emer_id: &str, - data: JsonUpcase<EmergencyAccessPasswordData>, + data: Json<EmergencyAccessPasswordData>, _headers: Headers, ) -> Error { Error::new(ACCESS_NOT_ALLOWED_MSG, ACCESS_NOT_ALLOWED_MSG) diff --git a/src/api/core/events.rs b/src/api/core/events.rs @@ -1,7 +1,4 @@ -use crate::{ - api::JsonUpcaseVec, - auth::{AdminHeaders, Headers}, -}; +use crate::auth::{AdminHeaders, Headers}; use rocket::{form::FromForm, serde::json::Json, Route}; use serde_json::Value; pub fn routes() -> Vec<Route> { @@ -9,10 +6,12 @@ pub fn routes() -> Vec<Route> { } #[derive(FromForm)] -#[allow(non_snake_case, dead_code)] struct EventRange { + #[allow(dead_code)] start: String, + #[allow(dead_code)] end: String, + #[allow(dead_code)] #[field(name = "continuationToken")] continuation_token: Option<String>, } @@ -22,9 +21,9 @@ struct EventRange { #[get("/organizations/<org_id>/events?<data..>")] fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": None::<&str>, + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": None::<&str>, })) } @@ -32,9 +31,9 @@ fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders) -> Jso #[get("/ciphers/<cipher_id>/events?<data..>")] fn get_cipher_events(cipher_id: &str, data: EventRange, _headers: Headers) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": None::<&str>, + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": None::<&str>, })) } @@ -47,9 +46,9 @@ fn get_user_events( _headers: AdminHeaders, ) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": None::<&str>, + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": None::<&str>, })) } pub fn main_routes() -> Vec<Route> { @@ -57,11 +56,10 @@ pub fn main_routes() -> Vec<Route> { } #[derive(Deserialize)] -#[allow(non_snake_case)] struct EventCollection; // Upstream: // https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Events/Controllers/CollectController.cs // https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Services/Implementations/EventService.cs #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/collect", format = "application/json", data = "<data>")] -fn post_events_collect(data: JsonUpcaseVec<EventCollection>, _headers: Headers) {} +fn post_events_collect(data: Json<Vec<EventCollection>>, _headers: Headers) {} diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs @@ -1,5 +1,5 @@ use crate::{ - api::{EmptyResult, JsonResult, JsonUpcase}, + api::{EmptyResult, JsonResult}, auth::Headers, db::{models::Folder, DbConn}, }; @@ -23,9 +23,9 @@ async fn get_folders(headers: Headers, conn: DbConn) -> Json<Value> { let folders = Folder::find_by_user(&headers.user.uuid, &conn).await; let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect(); Json(json!({ - "Data": folders_json, - "Object": "list", - "ContinuationToken": null, + "data": folders_json, + "object": "list", + "continuationToken": null, })) } @@ -41,15 +41,16 @@ async fn get_folder(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult { } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] pub struct FolderData { - pub Name: String, + pub name: String, + pub id: Option<String>, } #[post("/folders", data = "<data>")] -async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn) -> JsonResult { - let data: FolderData = data.into_inner().data; - let mut folder = Folder::new(headers.user.uuid, data.Name); +async fn post_folders(data: Json<FolderData>, headers: Headers, conn: DbConn) -> JsonResult { + let data: FolderData = data.into_inner(); + let mut folder = Folder::new(headers.user.uuid, data.name); folder.save(&conn).await?; Ok(Json(folder.to_json())) } @@ -57,7 +58,7 @@ async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbCo #[post("/folders/<uuid>", data = "<data>")] async fn post_folder( uuid: &str, - data: JsonUpcase<FolderData>, + data: Json<FolderData>, headers: Headers, conn: DbConn, ) -> JsonResult { @@ -67,18 +68,18 @@ async fn post_folder( #[put("/folders/<uuid>", data = "<data>")] async fn put_folder( uuid: &str, - data: JsonUpcase<FolderData>, + data: Json<FolderData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: FolderData = data.into_inner().data; + let data: FolderData = data.into_inner(); let Some(mut folder) = Folder::find_by_uuid(uuid, &conn).await else { err!("Invalid folder") }; if folder.user_uuid != headers.user.uuid { err!("Folder belongs to another user") } - folder.name = data.Name; + folder.name = data.name; folder.save(&conn).await?; Ok(Json(folder.to_json())) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -40,20 +40,16 @@ pub fn events_routes() -> Vec<Route> { // // Move this somewhere else // -use crate::{ - api::{JsonResult, JsonUpcase}, - auth::Headers, - db::DbConn, -}; +use crate::{api::JsonResult, auth::Headers, db::DbConn}; use rocket::{serde::json::Json, Catcher, Route}; use serde_json::Value; #[derive(Serialize, Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct GlobalDomain { - Type: i32, - Domains: Vec<String>, - Excluded: bool, + r#type: i32, + domains: Vec<String>, + excluded: bool, } const GLOBAL_DOMAINS: &str = include_str!("../../static/global_domains.json"); @@ -70,34 +66,34 @@ fn _get_eq_domains(headers: Headers, no_excluded: bool) -> Json<Value> { let excluded_globals: Vec<i32> = from_str(&user.excluded_globals).unwrap(); let mut globals: Vec<GlobalDomain> = from_str(GLOBAL_DOMAINS).unwrap(); for global in &mut globals { - global.Excluded = excluded_globals.contains(&global.Type); + global.excluded = excluded_globals.contains(&global.r#type); } if no_excluded { - globals.retain(|g| !g.Excluded); + globals.retain(|g| !g.excluded); } Json(json!({ - "EquivalentDomains": equivalent_domains, - "GlobalEquivalentDomains": globals, - "Object": "domains", + "equivalentDomains": equivalent_domains, + "globalEquivalentDomains": globals, + "object": "domains", })) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct EquivDomainData { - ExcludedGlobalEquivalentDomains: Option<Vec<i32>>, - EquivalentDomains: Option<Vec<Vec<String>>>, + excluded_global_equivalent_domains: Option<Vec<i32>>, + equivalent_domains: Option<Vec<Vec<String>>>, } #[post("/settings/domains", data = "<data>")] async fn post_eq_domains( - data: JsonUpcase<EquivDomainData>, + data: Json<EquivDomainData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: EquivDomainData = data.into_inner().data; - let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default(); - let equivalent_domains = data.EquivalentDomains.unwrap_or_default(); + let data: EquivDomainData = data.into_inner(); + let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default(); + let equivalent_domains = data.equivalent_domains.unwrap_or_default(); let mut user = headers.user; use serde_json::to_string; user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_owned()); @@ -107,11 +103,7 @@ async fn post_eq_domains( } #[put("/settings/domains", data = "<data>")] -async fn put_eq_domains( - data: JsonUpcase<EquivDomainData>, - headers: Headers, - conn: DbConn, -) -> JsonResult { +async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn) -> JsonResult { post_eq_domains(data, headers, conn).await } #[allow(unused_variables)] @@ -142,12 +134,11 @@ fn config() -> Json<Value> { // Note: The clients use this version to handle backwards compatibility concerns // This means they expect a version that closely matches the Bitwarden server version // We should make sure that we keep this updated when we support the new server features - "version": "2023.12.1", + "version": "2024.2.0", "gitHash": "", "server": { "name": "Vaultwarden", "url": "https://github.com/dani-garcia/vaultwarden", - "version": crate::VERSION }, "environment": { "vault": domain, @@ -157,9 +148,8 @@ fn config() -> Json<Value> { "sso": "" }, "featureStates": { - "autofill-overlay": true, - "autofill-v2": true, - "fido2-vault-credentials": true + "flexible-collections-v-1": false, + "key-rotation-improvements": true, }, "object": "config", })) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -1,8 +1,7 @@ use crate::{ api::{ core::{CipherSyncData, CipherSyncType}, - EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, NumberOrString, - PasswordOrOtpData, + EmptyResult, JsonResult, PasswordOrOtpData, }, auth::{self, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{ @@ -14,12 +13,12 @@ use crate::{ DbConn, }, error::Error, - util, + util::{self, NumberOrString}, }; use core::convert; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{http::Status, serde::json::Json, Route}; use serde_json::Value; +use std::collections::{HashMap, HashSet}; pub fn routes() -> Vec<Route> { routes![ @@ -58,7 +57,7 @@ pub fn routes() -> Vec<Route> { get_org_export, get_org_users, get_organization, - get_organization_keys, + get_organization_public_key, get_organization_tax, get_plans, get_plans_all, @@ -72,6 +71,7 @@ pub fn routes() -> Vec<Route> { leave_organization, list_policies, list_policies_token, + post_bulk_collections, post_delete_group, post_delete_group_user, post_delete_organization, @@ -105,69 +105,75 @@ pub fn routes() -> Vec<Route> { } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgData { - BillingEmail: String, - CollectionName: String, - Key: String, - Name: String, - Keys: Option<OrgKeyData>, - #[serde(rename = "PlanType")] - _PlanType: NumberOrString, // Ignored, always use the same plan + #[allow(dead_code)] + billing_email: String, + #[allow(dead_code)] + collection_name: String, + #[allow(dead_code)] + key: String, + #[allow(dead_code)] + name: String, + #[allow(dead_code)] + keys: Option<OrgKeyData>, + #[allow(dead_code)] + plan_type: NumberOrString, // Ignored, always use the same plan } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrganizationUpdateData { - BillingEmail: String, - Name: String, + billing_email: String, + name: String, } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct NewCollectionData { - Name: String, - Groups: Vec<NewCollectionObjectData>, - Users: Vec<NewCollectionObjectData>, - ExternalId: Option<String>, + name: String, + #[allow(dead_code)] + groups: Vec<NewCollectionObjectData>, + users: Vec<NewCollectionObjectData>, + external_id: Option<String>, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct NewCollectionObjectData { - HidePasswords: bool, - Id: String, - ReadOnly: bool, + hide_passwords: bool, + id: String, + read_only: bool, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgKeyData { - EncryptedPrivateKey: String, - PublicKey: String, + encrypted_private_key: String, + public_key: String, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgBulkIds { - Ids: Vec<String>, + ids: Vec<String>, } const ORG_CREATION_NOT_ALLOWED_MSG: &str = "Organization creation is not allowed"; #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations", data = "<data>")] -fn create_organization(_headers: Headers, data: JsonUpcase<OrgData>) -> Error { +fn create_organization(_headers: Headers, data: Json<OrgData>) -> Error { Error::new(ORG_CREATION_NOT_ALLOWED_MSG, ORG_CREATION_NOT_ALLOWED_MSG) } #[delete("/organizations/<org_id>", data = "<data>")] async fn delete_organization( org_id: &str, - data: JsonUpcase<PasswordOrOtpData>, + data: Json<PasswordOrOtpData>, headers: OwnerHeaders, conn: DbConn, ) -> EmptyResult { - let data: PasswordOrOtpData = data.into_inner().data; + let data: PasswordOrOtpData = data.into_inner(); data.validate(&headers.user)?; match Organization::find_by_uuid(org_id, &conn).await { None => err!("Organization not found"), @@ -178,7 +184,7 @@ async fn delete_organization( #[post("/organizations/<org_id>/delete", data = "<data>")] async fn post_delete_organization( org_id: &str, - data: JsonUpcase<PasswordOrOtpData>, + data: Json<PasswordOrOtpData>, headers: OwnerHeaders, conn: DbConn, ) -> EmptyResult { @@ -218,7 +224,7 @@ async fn get_organization(org_id: &str, _headers: OwnerHeaders, conn: DbConn) -> async fn put_organization( org_id: &str, headers: OwnerHeaders, - data: JsonUpcase<OrganizationUpdateData>, + data: Json<OrganizationUpdateData>, conn: DbConn, ) -> JsonResult { post_organization(org_id, headers, data, conn).await @@ -228,15 +234,15 @@ async fn put_organization( async fn post_organization( org_id: &str, _headers: OwnerHeaders, - data: JsonUpcase<OrganizationUpdateData>, + data: Json<OrganizationUpdateData>, conn: DbConn, ) -> JsonResult { - let data: OrganizationUpdateData = data.into_inner().data; + let data: OrganizationUpdateData = data.into_inner(); let Some(mut org) = Organization::find_by_uuid(org_id, &conn).await else { err!("Can't find organization details") }; - org.name = data.Name; - org.billing_email = data.BillingEmail; + org.name = data.name; + org.billing_email = data.billing_email; org.save(&conn).await?; Ok(Json(org.to_json())) } @@ -245,13 +251,13 @@ async fn post_organization( #[get("/collections")] async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> { Json(json!({ - "Data": + "data": Collection::find_by_user_uuid(headers.user.uuid.clone(), &conn).await .iter() .map(Collection::to_json) .collect::<Value>(), - "Object": "list", - "ContinuationToken": null, + "object": "list", + "continuationToken": null, })) } @@ -262,9 +268,9 @@ async fn get_org_collections( conn: DbConn, ) -> Json<Value> { Json(json!({ - "Data": _get_org_collections(org_id, &conn).await, - "Object": "list", - "ContinuationToken": null, + "data": _get_org_collections(org_id, &conn).await, + "object": "list", + "continuationToken": null, })) } @@ -281,35 +287,28 @@ async fn get_org_collections_details( err!("User is not part of organization") }; let coll_users = CollectionUser::find_by_organization(org_id, &conn).await; + let has_full_access_to_org = user_org.access_all; for col in Collection::find_by_organization(org_id, &conn).await { - let groups: Vec<Value> = Vec::new(); - let mut assigned = false; + let assigned = has_full_access_to_org; let users: Vec<Value> = coll_users .iter() .filter(|collection_user| collection_user.collection_uuid == col.uuid) .map(|collection_user| { - // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `CollectionUser::find_by_organization` call. - // We check here if the current user is assigned to this collection or not. - if collection_user.user_uuid == user_org.uuid { - assigned = true; - } SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json() }) .collect(); - if user_org.access_all { - assigned = true; - } + let groups = Vec::<Value>::new(); let mut json_object = col.to_json(); - json_object["Assigned"] = json!(assigned); - json_object["Users"] = json!(users); - json_object["Groups"] = json!(groups); - json_object["Object"] = json!("collectionAccessDetails"); + json_object["assigned"] = json!(assigned); + json_object["users"] = json!(users); + json_object["groups"] = json!(groups); + json_object["object"] = json!("collectionAccessDetails"); data.push(json_object); } Ok(Json(json!({ - "Data": data, - "Object": "list", - "ContinuationToken": null, + "data": data, + "object": "list", + "continuationToken": null, }))) } @@ -325,17 +324,17 @@ async fn _get_org_collections(org_id: &str, conn: &DbConn) -> Value { async fn post_organization_collections( org_id: &str, headers: ManagerHeadersLoose, - data: JsonUpcase<NewCollectionData>, + data: Json<NewCollectionData>, conn: DbConn, ) -> JsonResult { - let data: NewCollectionData = data.into_inner().data; + let data: NewCollectionData = data.into_inner(); let Some(org) = Organization::find_by_uuid(org_id, &conn).await else { err!("Can't find organization details") }; - let collection = Collection::new(org.uuid, data.Name, data.ExternalId); + let collection = Collection::new(org.uuid, data.name, data.external_id); collection.save(&conn).await?; - for user in data.Users { - let Some(org_user) = UserOrganization::find_by_uuid(&user.Id, &conn).await else { + for user in data.users { + let Some(org_user) = UserOrganization::find_by_uuid(&user.id, &conn).await else { err!("User is not part of organization") }; if org_user.access_all { @@ -344,8 +343,8 @@ async fn post_organization_collections( CollectionUser::save( &org_user.user_uuid, &collection.uuid, - user.ReadOnly, - user.HidePasswords, + user.read_only, + user.hide_passwords, &conn, ) .await?; @@ -368,7 +367,7 @@ async fn put_organization_collection_update( org_id: &str, col_id: &str, headers: ManagerHeaders, - data: JsonUpcase<NewCollectionData>, + data: Json<NewCollectionData>, conn: DbConn, ) -> JsonResult { post_organization_collection_update(org_id, col_id, headers, data, conn).await @@ -379,10 +378,10 @@ async fn post_organization_collection_update( org_id: &str, col_id: &str, _headers: ManagerHeaders, - data: JsonUpcase<NewCollectionData>, + data: Json<NewCollectionData>, conn: DbConn, ) -> JsonResult { - let data: NewCollectionData = data.into_inner().data; + let data: NewCollectionData = data.into_inner(); let Some(org) = Organization::find_by_uuid(org_id, &conn).await else { err!("Can't find organization details") }; @@ -392,15 +391,15 @@ async fn post_organization_collection_update( if collection.org_uuid != org.uuid { err!("Collection is not owned by organization"); } - collection.name = data.Name; - collection.external_id = match data.ExternalId { + collection.name = data.name; + collection.external_id = match data.external_id { Some(external_id) if !external_id.trim().is_empty() => Some(external_id), _ => None, }; collection.save(&conn).await?; CollectionUser::delete_all_by_collection(col_id, &conn).await?; - for user in data.Users { - let Some(org_user) = UserOrganization::find_by_uuid(&user.Id, &conn).await else { + for user in data.users { + let Some(org_user) = UserOrganization::find_by_uuid(&user.id, &conn).await else { err!("User is not part of organization") }; if org_user.access_all { @@ -409,8 +408,8 @@ async fn post_organization_collection_update( CollectionUser::save( &org_user.user_uuid, col_id, - user.ReadOnly, - user.HidePasswords, + user.read_only, + user.hide_passwords, &conn, ) .await?; @@ -493,10 +492,12 @@ async fn delete_organization_collection( } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct DeleteCollectionData { - Id: String, - OrgId: String, + #[allow(dead_code)] + id: String, + #[allow(dead_code)] + org_id: String, } #[post( @@ -507,31 +508,31 @@ async fn post_organization_collection_delete( org_id: &str, col_id: &str, headers: ManagerHeaders, - _data: JsonUpcase<DeleteCollectionData>, + _data: Json<DeleteCollectionData>, conn: DbConn, ) -> EmptyResult { _delete_organization_collection(org_id, col_id, &headers, &conn).await } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct BulkCollectionIds { - Ids: Vec<String>, - OrganizationId: String, + ids: Vec<String>, + organization_id: String, } #[delete("/organizations/<org_id>/collections", data = "<data>")] async fn bulk_delete_organization_collections( org_id: &str, headers: ManagerHeadersLoose, - data: JsonUpcase<BulkCollectionIds>, + data: Json<BulkCollectionIds>, conn: DbConn, ) -> EmptyResult { - let data: BulkCollectionIds = data.into_inner().data; - if org_id != data.OrganizationId { + let data: BulkCollectionIds = data.into_inner(); + if org_id != data.organization_id { err!("OrganizationId mismatch"); } - let collections = data.Ids; + let collections = data.ids; let headers = ManagerHeaders::from_loose(headers, &collections, &conn).await?; for col_id in collections { _delete_organization_collection(org_id, &col_id, &headers, &conn).await?; @@ -558,7 +559,6 @@ async fn get_org_collection_detail( err!("User is not part of organization") }; let groups: Vec<Value> = Vec::new(); - let mut assigned = false; let users: Vec<Value> = CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid( &collection.uuid, @@ -567,24 +567,17 @@ async fn get_org_collection_detail( .await .iter() .map(|collection_user| { - // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `find_by_collection_swap_user_uuid_with_org_user_uuid` call. - // We check here if the current user is assigned to this collection or not. - if collection_user.user_uuid == user_org.uuid { - assigned = true; - } SelectionReadOnly::to_collection_user_details_read_only(collection_user) .to_json() }) .collect(); - - if user_org.access_all { - assigned = true; - } + let assigned = + Collection::can_access_collection(&user_org, &collection.uuid, &conn).await; let mut json_object = collection.to_json(); - json_object["Assigned"] = json!(assigned); - json_object["Users"] = json!(users); - json_object["Groups"] = json!(groups); - json_object["Object"] = json!("collectionAccessDetails"); + json_object["assigned"] = json!(assigned); + json_object["users"] = json!(users); + json_object["groups"] = json!(groups); + json_object["object"] = json!("collectionAccessDetails"); Ok(Json(json_object)) } } @@ -618,7 +611,7 @@ async fn get_collection_users( async fn put_collection_users( org_id: &str, coll_id: &str, - data: JsonUpcaseVec<CollectionData>, + data: Json<Vec<CollectionData>>, _headers: ManagerHeaders, conn: DbConn, ) -> EmptyResult { @@ -632,14 +625,21 @@ async fn put_collection_users( // Delete all the user-collections CollectionUser::delete_all_by_collection(coll_id, &conn).await?; // And then add all the received ones (except if the user has access_all) - for d in data.iter().map(|d| &d.data) { - let Some(user) = UserOrganization::find_by_uuid(&d.Id, &conn).await else { + for d in data.iter() { + let Some(user) = UserOrganization::find_by_uuid(&d.id, &conn).await else { err!("User is not part of organization") }; if user.access_all { continue; } - CollectionUser::save(&user.user_uuid, coll_id, d.ReadOnly, d.HidePasswords, &conn).await?; + CollectionUser::save( + &user.user_uuid, + coll_id, + d.read_only, + d.hide_passwords, + &conn, + ) + .await?; } Ok(()) } @@ -651,12 +651,23 @@ struct OrgIdData { } #[get("/ciphers/organization-details?<data..>")] -async fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> Json<Value> { - Json(json!({ - "Data": _get_org_details(&data.organization_id, &headers.user.uuid, &conn).await, - "Object": "list", - "ContinuationToken": null, - })) +async fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResult { + if UserOrganization::find_confirmed_by_user_and_org( + &headers.user.uuid, + &data.organization_id, + &conn, + ) + .await + .is_none() + { + err_code!("Resource not found.", Status::NotFound.code); + } + + Ok(Json(json!({ + "data": _get_org_details(&data.organization_id, &headers.user.uuid, &conn).await, + "object": "list", + "continuationToken": null, + }))) } async fn _get_org_details(org_id: &str, user_uuid: &str, conn: &DbConn) -> Value { @@ -700,20 +711,20 @@ async fn get_org_users( ); } Json(json!({ - "Data": users_json, - "Object": "list", - "ContinuationToken": null, + "data": users_json, + "object": "list", + "continuationToken": null, })) } #[post("/organizations/<org_id>/keys", data = "<data>")] async fn post_org_keys( org_id: &str, - data: JsonUpcase<OrgKeyData>, + data: Json<OrgKeyData>, _headers: AdminHeaders, conn: DbConn, ) -> JsonResult { - let data: OrgKeyData = data.into_inner().data; + let data: OrgKeyData = data.into_inner(); let mut org = match Organization::find_by_uuid(org_id, &conn).await { Some(organization) => { if organization.private_key.is_some() && organization.public_key.is_some() { @@ -723,50 +734,52 @@ async fn post_org_keys( } None => err!("Can't find organization details"), }; - org.private_key = Some(data.EncryptedPrivateKey); - org.public_key = Some(data.PublicKey); + org.private_key = Some(data.encrypted_private_key); + org.public_key = Some(data.public_key); org.save(&conn).await?; Ok(Json(json!({ - "Object": "organizationKeys", - "PublicKey": org.public_key, - "PrivateKey": org.private_key, + "object": "organizationKeys", + "publicKey": org.public_key, + "privateKey": org.private_key, }))) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct CollectionData { - Id: String, - ReadOnly: bool, - HidePasswords: bool, + id: String, + read_only: bool, + hide_passwords: bool, } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct InviteData { - Emails: Vec<String>, - Groups: Vec<String>, - Type: NumberOrString, - Collections: Option<Vec<CollectionData>>, - AccessAll: Option<bool>, + #[allow(dead_code)] + emails: Vec<String>, + #[allow(dead_code)] + groups: Vec<String>, + #[allow(dead_code)] + r#type: NumberOrString, + #[allow(dead_code)] + collections: Option<Vec<CollectionData>>, + #[allow(dead_code)] + #[serde(default)] + access_all: bool, } const INVITATIONS_NOT_ALLOWED_MSG: &str = "Invitations are not allowed."; #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations/<org_id>/users/invite", data = "<data>")] -fn send_invite(org_id: &str, data: JsonUpcase<InviteData>, _headers: AdminHeaders) -> Error { +fn send_invite(org_id: &str, data: Json<InviteData>, _headers: AdminHeaders) -> Error { Error::new(INVITATIONS_NOT_ALLOWED_MSG, INVITATIONS_NOT_ALLOWED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations/<org_id>/users/reinvite", data = "<data>")] -fn bulk_reinvite_user( - org_id: &str, - data: JsonUpcase<OrgBulkIds>, - _headers: AdminHeaders, -) -> Json<Value> { +fn bulk_reinvite_user(org_id: &str, data: Json<OrgBulkIds>, _headers: AdminHeaders) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": null + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": null })) } @@ -777,29 +790,27 @@ fn reinvite_user(org_id: &str, user_org: &str, _headers: AdminHeaders) -> Error } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct AcceptData { - Token: String, - ResetPasswordKey: Option<String>, + #[allow(dead_code)] + token: String, + #[allow(dead_code)] + reset_password_key: Option<String>, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")] -fn accept_invite(org_id: &str, _org_user_id: &str, data: JsonUpcase<AcceptData>) -> Error { +fn accept_invite(org_id: &str, _org_user_id: &str, data: Json<AcceptData>) -> Error { Error::new(INVITATIONS_NOT_ALLOWED_MSG, INVITATIONS_NOT_ALLOWED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations/<org_id>/users/confirm", data = "<data>")] -fn bulk_confirm_invite( - org_id: &str, - data: JsonUpcase<Value>, - _headers: AdminHeaders, -) -> Json<Value> { +fn bulk_confirm_invite(org_id: &str, data: Json<Value>, _headers: AdminHeaders) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": null + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": null })) } @@ -808,7 +819,7 @@ fn bulk_confirm_invite( fn confirm_invite( org_id: &str, org_user_id: &str, - data: JsonUpcase<Value>, + data: Json<Value>, _headers: AdminHeaders, ) -> Error { Error::new(INVITATIONS_NOT_ALLOWED_MSG, INVITATIONS_NOT_ALLOWED_MSG) @@ -836,12 +847,13 @@ async fn get_user( } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct EditUserData { - Type: NumberOrString, - Collections: Option<Vec<CollectionData>>, - Groups: Option<Vec<String>>, - AccessAll: bool, + r#type: NumberOrString, + collections: Option<Vec<CollectionData>>, + #[allow(dead_code)] + groups: Option<Vec<String>>, + access_all: bool, } #[put( @@ -852,7 +864,7 @@ struct EditUserData { async fn put_organization_user( org_id: &str, org_user_id: &str, - data: JsonUpcase<EditUserData>, + data: Json<EditUserData>, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { @@ -867,12 +879,12 @@ async fn put_organization_user( async fn edit_user( org_id: &str, org_user_id: &str, - data: JsonUpcase<EditUserData>, + data: Json<EditUserData>, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - let data: EditUserData = data.into_inner().data; - let Some(new_type) = UserOrgType::from_str(&data.Type.into_string()) else { + let data: EditUserData = data.into_inner(); + let Some(new_type) = UserOrgType::from_str(&data.r#type.into_string()) else { err!("Invalid type") }; let Some(mut user_to_edit) = @@ -914,7 +926,7 @@ async fn edit_user( } } } - user_to_edit.access_all = data.AccessAll; + user_to_edit.access_all = data.access_all; user_to_edit.atype = i32::from(new_type); // Delete all the odd collections for c in @@ -924,16 +936,16 @@ async fn edit_user( c.delete(&conn).await?; } // If no accessAll, add the collections received - if !data.AccessAll { - for col in data.Collections.iter().flatten() { - match Collection::find_by_uuid_and_org(&col.Id, org_id, &conn).await { + if !data.access_all { + for col in data.collections.iter().flatten() { + match Collection::find_by_uuid_and_org(&col.id, org_id, &conn).await { None => err!("Collection not found in Organization"), Some(collection) => { CollectionUser::save( &user_to_edit.user_uuid, &collection.uuid, - col.ReadOnly, - col.HidePasswords, + col.read_only, + col.hide_passwords, &conn, ) .await?; @@ -947,29 +959,29 @@ async fn edit_user( #[delete("/organizations/<org_id>/users", data = "<data>")] async fn bulk_delete_user( org_id: &str, - data: JsonUpcase<OrgBulkIds>, + data: Json<OrgBulkIds>, headers: AdminHeaders, conn: DbConn, ) -> Json<Value> { - let data: OrgBulkIds = data.into_inner().data; + let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); - for org_user_id in data.Ids { + for org_user_id in data.ids { let err_msg = match _delete_user(org_id, &org_user_id, &headers, &conn).await { Ok(()) => String::new(), Err(e) => format!("{e:?}"), }; bulk_response.push(json!( { - "Object": "OrganizationBulkConfirmResponseModel", - "Id": org_user_id, - "Error": err_msg + "object": "organizationBulkConfirmResponseModel", + "id": org_user_id, + "error": err_msg } )); } Json(json!({ - "Data": bulk_response, - "Object": "list", - "ContinuationToken": null + "data": bulk_response, + "object": "list", + "continuationToken": null })) } @@ -1023,26 +1035,26 @@ async fn _delete_user( #[post("/organizations/<org_id>/users/public-keys", data = "<data>")] async fn bulk_public_keys( org_id: &str, - data: JsonUpcase<OrgBulkIds>, + data: Json<OrgBulkIds>, _headers: AdminHeaders, conn: DbConn, ) -> Json<Value> { - let data: OrgBulkIds = data.into_inner().data; + let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); // Check all received UserOrg UUID's and find the matching User to retrieve the public-key. // If the user does not exists, just ignore it, and do not return any information regarding that UserOrg UUID. // The web-vault will then ignore that user for the following steps. - for user_org_id in data.Ids { + for user_org_id in data.ids { if let Some(user_org) = UserOrganization::find_by_uuid_and_org(&user_org_id, org_id, &conn).await { if let Some(user) = User::find_by_uuid(&user_org.user_uuid, &conn).await { bulk_response.push(json!( { - "Object": "organizationUserPublicKeyResponseModel", - "Id": user_org_id, - "UserId": user.uuid, - "Key": user.public_key + "object": "organizationUserPublicKeyResponseModel", + "id": user_org_id, + "userId": user.uuid, + "key": user.public_key } )); } else { @@ -1053,9 +1065,9 @@ async fn bulk_public_keys( } } Json(json!({ - "Data": bulk_response, - "Object": "list", - "ContinuationToken": null + "data": bulk_response, + "object": "list", + "continuationToken": null })) } @@ -1063,39 +1075,39 @@ use super::ciphers::update_cipher_from_data; use super::ciphers::CipherData; #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ImportData { - Ciphers: Vec<CipherData>, - Collections: Vec<NewCollectionData>, - CollectionRelationships: Vec<RelationsData>, + ciphers: Vec<CipherData>, + collections: Vec<NewCollectionData>, + collection_relationships: Vec<RelationsData>, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct RelationsData { // Cipher index - Key: usize, + key: usize, // Collection index - Value: usize, + value: usize, } #[post("/ciphers/import-organization?<query..>", data = "<data>")] async fn post_org_import( query: OrgIdData, - data: JsonUpcase<ImportData>, + data: Json<ImportData>, headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { - let data: ImportData = data.into_inner().data; + let data: ImportData = data.into_inner(); let org_id = query.organization_id; // Validate the import before continuing // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_notes(&data.Ciphers)?; + Cipher::validate_notes(&data.ciphers)?; let mut collections = Vec::new(); - for coll in data.Collections { - let collection = Collection::new(org_id.clone(), coll.Name, coll.ExternalId); + for coll in data.collections { + let collection = Collection::new(org_id.clone(), coll.name, coll.external_id); if collection.save(&conn).await.is_err() { collections.push(Err(Error::new( "Failed to create Collection", @@ -1107,14 +1119,14 @@ async fn post_org_import( } // Read the relations between collections and ciphers let mut relations = Vec::new(); - for relation in data.CollectionRelationships { - relations.push((relation.Key, relation.Value)); + for relation in data.collection_relationships { + relations.push((relation.key, relation.value)); } let headers: Headers = headers.into(); let mut ciphers = Vec::new(); - for cipher_data in data.Ciphers { - let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone()); - update_cipher_from_data(&mut cipher, cipher_data, &headers, false, &conn, true) + for cipher_data in data.ciphers { + let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone()); + update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &conn, true) .await .ok(); ciphers.push(cipher); @@ -1134,19 +1146,102 @@ async fn post_org_import( user.update_revision(&conn).await } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct BulkCollectionsData { + organization_id: String, + cipher_ids: Vec<String>, + collection_ids: HashSet<String>, + remove_collections: bool, +} + +// This endpoint is only reachable via the organization view, therefor this endpoint is located here +// Also Bitwarden does not send out Notifications for these changes, it only does this for individual cipher collection updates +#[allow(clippy::iter_over_hash_type)] +#[post("/ciphers/bulk-collections", data = "<data>")] +async fn post_bulk_collections( + data: Json<BulkCollectionsData>, + headers: Headers, + conn: DbConn, +) -> EmptyResult { + let data: BulkCollectionsData = data.into_inner(); + + // This feature does not seem to be active on all the clients + // To prevent future issues, add a check to block a call when this is set to true + if data.remove_collections { + err!("Bulk removing of collections is not yet implemented") + } + + // Get all the collection available to the user in one query + // Also filter based upon the provided collections + let user_collections: HashMap<String, Collection> = + Collection::find_by_organization_and_user_uuid( + &data.organization_id, + &headers.user.uuid, + &conn, + ) + .await + .into_iter() + .filter_map(|c| { + if data.collection_ids.contains(&c.uuid) { + Some((c.uuid.clone(), c)) + } else { + None + } + }) + .collect(); + + // Verify if all the collections requested exists and are writeable for the user, else abort + for collection_uuid in &data.collection_ids { + match user_collections.get(collection_uuid) { + Some(collection) + if collection + .is_writable_by_user(&headers.user.uuid, &conn) + .await => {} + _ => err_code!( + "Resource not found", + "User does not have access to a collection", + 404 + ), + } + } + + for cipher_id in &data.cipher_ids { + // Only act on existing cipher uuid's + // Do not abort the operation just ignore it, it could be a cipher was just deleted for example + if let Some(cipher) = + Cipher::find_by_uuid_and_org(cipher_id, &data.organization_id, &conn).await + { + if cipher + .is_write_accessible_to_user(&headers.user.uuid, &conn) + .await + { + for collection in &data.collection_ids { + CollectionCipher::save(&cipher.uuid, collection, &conn).await?; + } + } + }; + } + + Ok(()) +} + #[get("/organizations/<org_id>/policies")] async fn list_policies(org_id: &str, _headers: AdminHeaders, conn: DbConn) -> Json<Value> { let policies = OrgPolicy::find_by_org(org_id, &conn).await; let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect(); Json(json!({ - "Data": policies_json, - "Object": "list", - "ContinuationToken": null + "data": policies_json, + "object": "list", + "continuationToken": null })) } #[get("/organizations/<org_id>/policies/token?<token>")] async fn list_policies_token(org_id: &str, token: &str, conn: DbConn) -> JsonResult { + if org_id == "undefined" && token == "undefined" { + return Ok(Json(json!({}))); + } let invite = auth::decode_invite(token)?; let Some(invite_org_id) = invite.org_id else { err!("Invalid token") @@ -1158,9 +1253,9 @@ async fn list_policies_token(org_id: &str, token: &str, conn: DbConn) -> JsonRes let policies = OrgPolicy::find_by_org(org_id, &conn).await; let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect(); Ok(Json(json!({ - "Data": policies_json, - "Object": "list", - "ContinuationToken": null + "data": policies_json, + "object": "list", + "continuationToken": null }))) } @@ -1256,25 +1351,25 @@ fn get_organization_tax(org_id: &str, _headers: Headers) -> Json<Value> { fn get_plans() -> Json<Value> { // Respond with a minimal json just enough to allow the creation of an new organization. Json(json!({ - "Object": "list", - "Data": [{ - "Object": "plan", - "Type": 0i32, - "Product": 0i32, - "Name": "Free", - "NameLocalizationKey": "planNameFree", - "BitwardenProduct": 0i32, - "MaxUsers": 0i32, - "DescriptionLocalizationKey": "planDescFree" + "object": "list", + "data": [{ + "object": "plan", + "type": 0i32, + "product": 0i32, + "name": "Free", + "nameLocalizationKey": "planNameFree", + "bitwardenProduct": 0i32, + "maxUsers": 0i32, + "descriptionLocalizationKey": "planDescFree" },{ - "Object": "plan", - "Type": 0i32, - "Product": 1i32, - "Name": "Free", - "NameLocalizationKey": "planNameFree", - "BitwardenProduct": 1i32, - "MaxUsers": 0i32, - "DescriptionLocalizationKey": "planDescFree" + "object": "plan", + "type": 0i32, + "product": 1i32, + "name": "Free", + "nameLocalizationKey": "planNameFree", + "bitwardenProduct": 1i32, + "maxUsers": 0i32, + "descriptionLocalizationKey": "planDescFree" }], "ContinuationToken": null })) @@ -1294,46 +1389,49 @@ fn get_plans_tax_rates(_headers: Headers) -> Json<Value> { fn _empty_data_json() -> Value { json!({ - "Object": "list", - "Data": [], - "ContinuationToken": null + "object": "list", + "data": [], + "continuationToken": null }) } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct OrgImportGroupData { - Name: String, // "GroupName" - ExternalId: String, // "cn=GroupName,ou=Groups,dc=example,dc=com" - Users: Vec<String>, // ["uid=user,ou=People,dc=example,dc=com"] + #[allow(dead_code)] + name: String, // "GroupName" + #[allow(dead_code)] + external_id: String, // "cn=GroupName,ou=Groups,dc=example,dc=com" + #[allow(dead_code)] + users: Vec<String>, // ["uid=user,ou=People,dc=example,dc=com"] } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgImportUserData { - Email: String, // "user@maildomain.net" + email: String, // "user@maildomain.net" #[allow(dead_code)] - ExternalId: String, // "uid=user,ou=People,dc=example,dc=com" - Deleted: bool, + external_id: String, // "uid=user,ou=People,dc=example,dc=com" + deleted: bool, } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgImportData { #[allow(dead_code)] - Groups: Vec<OrgImportGroupData>, - OverwriteExisting: bool, - Users: Vec<OrgImportUserData>, + groups: Vec<OrgImportGroupData>, + overwrite_existing: bool, + users: Vec<OrgImportUserData>, } #[post("/organizations/<org_id>/import", data = "<data>")] async fn import( org_id: &str, - data: JsonUpcase<OrgImportData>, + data: Json<OrgImportData>, headers: Headers, conn: DbConn, ) -> EmptyResult { - let data = data.into_inner().data; + let data = data.into_inner(); // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way // to differentiate between auto-imported users and manually added ones. // This means that this endpoint can end up removing users that were added manually by an admin, @@ -1344,21 +1442,21 @@ async fn import( Some(_) => err!("User has insufficient permissions to use Directory Connector"), None => err!("User not part of organization"), }; - for user_data in &data.Users { - if user_data.Deleted { + for user_data in &data.users { + if user_data.deleted { // If user is marked for deletion and it exists, delete it if let Some(user_org) = - UserOrganization::find_by_email_and_org(&user_data.Email, org_id, &conn).await + UserOrganization::find_by_email_and_org(&user_data.email, org_id, &conn).await { user_org.delete(&conn).await?; } // If user is not part of the organization, but it exists - } else if UserOrganization::find_by_email_and_org(&user_data.Email, org_id, &conn) + } else if UserOrganization::find_by_email_and_org(&user_data.email, org_id, &conn) .await .is_none() { - if let Some(user) = User::find_by_mail(&user_data.Email, &conn).await { + if let Some(user) = User::find_by_mail(&user_data.email, &conn).await { let user_org_status = i32::from(UserOrgStatus::Accepted); let mut new_org_user = UserOrganization::new(user.uuid.clone(), String::from(org_id)); @@ -1372,7 +1470,7 @@ async fn import( } } // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) - if data.OverwriteExisting { + if data.overwrite_existing { for user_org in UserOrganization::find_by_org_and_type(org_id, UserOrgType::User, &conn).await { @@ -1380,7 +1478,7 @@ async fn import( .await .map(|u| u.email) { - if !data.Users.iter().any(|u| u.Email == user_email) { + if !data.users.iter().any(|u| u.email == user_email) { user_org.delete(&conn).await?; } } @@ -1404,7 +1502,7 @@ async fn deactivate_organization_user( #[put("/organizations/<org_id>/users/deactivate", data = "<data>")] async fn bulk_deactivate_organization_user( org_id: &str, - data: JsonUpcase<Value>, + data: Json<Value>, headers: AdminHeaders, conn: DbConn, ) -> Json<Value> { @@ -1424,11 +1522,11 @@ async fn revoke_organization_user( #[put("/organizations/<org_id>/users/revoke", data = "<data>")] async fn bulk_revoke_organization_user( org_id: &str, - data: JsonUpcase<Value>, + data: Json<Value>, headers: AdminHeaders, conn: DbConn, ) -> Json<Value> { - let data = data.into_inner().data; + let data = data.into_inner(); let mut bulk_response = Vec::new(); match data["Ids"].as_array() { Some(org_users) => { @@ -1441,9 +1539,9 @@ async fn bulk_revoke_organization_user( }; bulk_response.push(json!( { - "Object": "OrganizationUserBulkResponseModel", - "Id": org_user_id, - "Error": err_msg + "object": "organizationUserBulkResponseModel", + "id": org_user_id, + "error": err_msg } )); } @@ -1451,9 +1549,9 @@ async fn bulk_revoke_organization_user( None => panic!("No users to revoke"), } Json(json!({ - "Data": bulk_response, - "Object": "list", - "ContinuationToken": null + "data": bulk_response, + "object": "list", + "continuationToken": null })) } @@ -1506,7 +1604,7 @@ async fn activate_organization_user( #[put("/organizations/<org_id>/users/activate", data = "<data>")] async fn bulk_activate_organization_user( org_id: &str, - data: JsonUpcase<Value>, + data: Json<OrgBulkIds>, headers: AdminHeaders, conn: DbConn, ) -> Json<Value> { @@ -1526,36 +1624,30 @@ async fn restore_organization_user( #[put("/organizations/<org_id>/users/restore", data = "<data>")] async fn bulk_restore_organization_user( org_id: &str, - data: JsonUpcase<Value>, + data: Json<OrgBulkIds>, headers: AdminHeaders, conn: DbConn, ) -> Json<Value> { - let data = data.into_inner().data; + let data = data.into_inner(); let mut bulk_response = Vec::new(); - match data["Ids"].as_array() { - Some(org_users) => { - for org_user_id in org_users { - let org_user_id = org_user_id.as_str().unwrap_or_default(); - let err_msg = - match _restore_organization_user(org_id, org_user_id, &headers, &conn).await { - Ok(()) => String::new(), - Err(e) => format!("{e:?}"), - }; - bulk_response.push(json!( - { - "Object": "OrganizationUserBulkResponseModel", - "Id": org_user_id, - "Error": err_msg - } - )); + for org_user_id in data.ids { + let err_msg = match _restore_organization_user(org_id, &org_user_id, &headers, &conn).await + { + Ok(()) => String::new(), + Err(e) => format!("{e:?}"), + }; + bulk_response.push(json!( + { + "object": "OrganizationUserBulkResponseModel", + "id": org_user_id, + "error": err_msg } - } - None => panic!("No users to restore"), + )); } Json(json!({ - "Data": bulk_response, - "Object": "list", - "ContinuationToken": null + "data": bulk_response, + "object": "list", + "continuationToken": null })) } @@ -1599,29 +1691,29 @@ async fn _restore_organization_user( #[get("/organizations/<org_id>/groups")] fn get_groups(org_id: &str, _headers: ManagerHeadersLoose) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": null, + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": null, })) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct GroupRequest; #[derive(Serialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct SelectionReadOnly { - Id: String, - ReadOnly: bool, - HidePasswords: bool, + id: String, + read_only: bool, + hide_passwords: bool, } impl SelectionReadOnly { fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> Self { Self { - Id: collection_user.user_uuid.clone(), - ReadOnly: collection_user.read_only, - HidePasswords: collection_user.hide_passwords, + id: collection_user.user_uuid.clone(), + read_only: collection_user.read_only, + hide_passwords: collection_user.hide_passwords, } } fn to_json(&self) -> Value { @@ -1635,7 +1727,7 @@ const GROUPS_DISABLED_MSG: &str = "Groups are disabled."; fn post_group( org_id: &str, group_id: &str, - data: JsonUpcase<GroupRequest>, + data: Json<GroupRequest>, _headers: AdminHeaders, ) -> Error { Error::new(GROUPS_DISABLED_MSG, GROUPS_DISABLED_MSG) @@ -1643,7 +1735,7 @@ fn post_group( #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations/<org_id>/groups", data = "<data>")] -fn post_groups(org_id: &str, _headers: AdminHeaders, data: JsonUpcase<GroupRequest>) -> Error { +fn post_groups(org_id: &str, _headers: AdminHeaders, data: Json<GroupRequest>) -> Error { Error::new(GROUPS_DISABLED_MSG, GROUPS_DISABLED_MSG) } @@ -1652,7 +1744,7 @@ fn post_groups(org_id: &str, _headers: AdminHeaders, data: JsonUpcase<GroupReque fn put_group( org_id: &str, group_id: &str, - data: JsonUpcase<GroupRequest>, + data: Json<GroupRequest>, _headers: AdminHeaders, ) -> Error { Error::new(GROUPS_DISABLED_MSG, GROUPS_DISABLED_MSG) @@ -1677,7 +1769,7 @@ fn delete_group(org_id: &str, group_id: &str, _headers: AdminHeaders) -> Error { } #[allow(unused_variables, clippy::needless_pass_by_value)] #[delete("/organizations/<org_id>/groups", data = "<data>")] -fn bulk_delete_groups(org_id: &str, data: JsonUpcase<OrgBulkIds>, _headers: AdminHeaders) -> Error { +fn bulk_delete_groups(org_id: &str, data: Json<OrgBulkIds>, _headers: AdminHeaders) -> Error { Error::new(GROUPS_DISABLED_MSG, GROUPS_DISABLED_MSG) } @@ -1698,7 +1790,7 @@ fn put_group_users( org_id: &str, group_id: &str, _headers: AdminHeaders, - data: JsonVec<String>, + data: Json<Vec<String>>, ) -> Error { Error::new(GROUPS_DISABLED_MSG, GROUPS_DISABLED_MSG) } @@ -1710,7 +1802,7 @@ fn get_user_groups(_org_id: &str, user_id: &str, _headers: AdminHeaders) -> Erro } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrganizationUserUpdateGroupsRequest; #[allow(unused_variables, clippy::needless_pass_by_value)] @@ -1718,7 +1810,7 @@ struct OrganizationUserUpdateGroupsRequest; fn post_user_groups( org_id: &str, org_user_id: &str, - data: JsonUpcase<OrganizationUserUpdateGroupsRequest>, + data: Json<OrganizationUserUpdateGroupsRequest>, _headers: AdminHeaders, ) -> Error { Error::new(GROUPS_DISABLED_MSG, GROUPS_DISABLED_MSG) @@ -1729,7 +1821,7 @@ fn post_user_groups( fn put_user_groups( org_id: &str, org_user_id: &str, - data: JsonUpcase<OrganizationUserUpdateGroupsRequest>, + data: Json<OrganizationUserUpdateGroupsRequest>, _headers: AdminHeaders, ) -> Error { Error::new(GROUPS_DISABLED_MSG, GROUPS_DISABLED_MSG) @@ -1758,28 +1850,33 @@ fn delete_group_user( } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrganizationUserResetPasswordEnrollmentRequest { - ResetPasswordKey: Option<String>, - MasterPasswordHash: Option<String>, + #[allow(dead_code)] + reset_password_key: Option<String>, + #[allow(dead_code)] + master_password_hash: Option<String>, + #[allow(dead_code)] + otp: Option<String>, } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrganizationUserResetPasswordRequest { - NewMasterPasswordHash: String, - Key: String, + #[allow(dead_code)] + new_master_password_hash: String, + #[allow(dead_code)] + key: String, } - -#[get("/organizations/<org_id>/keys")] -async fn get_organization_keys(org_id: &str, conn: DbConn) -> JsonResult { +#[get("/organizations/<org_id>/public-key")] +async fn get_organization_public_key(org_id: &str, _headers: Headers, conn: DbConn) -> JsonResult { let Some(org) = Organization::find_by_uuid(org_id, &conn).await else { err!("Organization not found") }; + Ok(Json(json!({ - "Object": "organizationKeys", - "PublicKey": org.public_key, - "PrivateKey": org.private_key, + "object": "organizationPublicKey", + "publicKey": org.public_key, }))) } const PASS_RESET_MSG: &str = "Password reset is not supported on an email-disabled instance."; @@ -1792,7 +1889,7 @@ async fn put_reset_password( org_id: &str, org_user_id: &str, _headers: AdminHeaders, - data: JsonUpcase<OrganizationUserResetPasswordRequest>, + data: Json<OrganizationUserResetPasswordRequest>, conn: DbConn, ) -> EmptyResult { let Some(org) = Organization::find_by_uuid(org_id, &conn).await else { @@ -1824,7 +1921,7 @@ fn put_reset_password_enrollment( org_id: &str, org_user_id: &str, _headers: Headers, - data: JsonUpcase<OrganizationUserResetPasswordEnrollmentRequest>, + data: Json<OrganizationUserResetPasswordEnrollmentRequest>, ) -> Error { Error::new(PASS_RESET_MSG, PASS_RESET_MSG) } @@ -1875,15 +1972,11 @@ async fn get_org_export(org_id: &str, headers: AdminHeaders, conn: DbConn) -> Js const API_DISABLED_MSG: &str = "API access is disabled."; #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations/<org_id>/api-key", data = "<data>")] -fn api_key(org_id: &str, data: JsonUpcase<PasswordOrOtpData>, _headers: AdminHeaders) -> Error { +fn api_key(org_id: &str, data: Json<PasswordOrOtpData>, _headers: AdminHeaders) -> Error { Error::new(API_DISABLED_MSG, API_DISABLED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/organizations/<org_id>/rotate-api-key", data = "<data>")] -fn rotate_api_key( - org_id: &str, - data: JsonUpcase<PasswordOrOtpData>, - _headers: AdminHeaders, -) -> Error { +fn rotate_api_key(org_id: &str, data: Json<PasswordOrOtpData>, _headers: AdminHeaders) -> Error { Error::new(API_DISABLED_MSG, API_DISABLED_MSG) } diff --git a/src/api/core/public.rs b/src/api/core/public.rs @@ -1,32 +1,37 @@ -use crate::{api::JsonUpcase, error::Error}; -use rocket::Route; +use crate::error::Error; +use rocket::{serde::json::Json, Route}; pub fn routes() -> Vec<Route> { routes![ldap_import] } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgImportGroupData; #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgImportUserData { - Email: String, - ExternalId: String, - Deleted: bool, + #[allow(dead_code)] + email: String, + #[allow(dead_code)] + external_id: String, + #[allow(dead_code)] + deleted: bool, } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct OrgImportData { - Members: Vec<OrgImportUserData>, - OverwriteExisting: bool, + #[allow(dead_code)] + members: Vec<OrgImportUserData>, + #[allow(dead_code)] + overwrite_existing: bool, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/public/organization/import", data = "<data>")] -fn ldap_import(data: JsonUpcase<OrgImportData>) -> Error { +fn ldap_import(data: Json<OrgImportData>) -> Error { const MSG: &str = "LDAP import is permanently disabled."; Error::new(MSG, MSG) } diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs @@ -1,13 +1,14 @@ use crate::{ - api::{JsonUpcase, NumberOrString}, auth::Headers, error::Error, - util::{SafeString, UpCase}, + util::{NumberOrString, SafeString}, }; use chrono::{DateTime, Utc}; -use rocket::form::Form; -use rocket::fs::{NamedFile, TempFile}; -use rocket::serde::json::Json; +use rocket::{ + form::Form, + fs::{NamedFile, TempFile}, + serde::json::Json, +}; use serde_json::Value; pub fn routes() -> Vec<rocket::Route> { @@ -28,30 +29,45 @@ pub fn routes() -> Vec<rocket::Route> { } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct SendData { - Type: i32, - Key: String, - Password: Option<String>, - MaxAccessCount: Option<NumberOrString>, - ExpirationDate: Option<DateTime<Utc>>, - DeletionDate: DateTime<Utc>, - Disabled: bool, - HideEmail: Option<bool>, - Name: String, - Notes: Option<String>, - Text: Option<Value>, - File: Option<Value>, - FileLength: Option<NumberOrString>, + #[allow(dead_code)] + r#type: i32, + #[allow(dead_code)] + key: String, + #[allow(dead_code)] + password: Option<String>, + #[allow(dead_code)] + max_access_count: Option<NumberOrString>, + #[allow(dead_code)] + expiration_date: Option<DateTime<Utc>>, + #[allow(dead_code)] + deletion_date: DateTime<Utc>, + #[allow(dead_code)] + disabled: bool, + #[allow(dead_code)] + hide_email: Option<bool>, + #[allow(dead_code)] + name: String, + #[allow(dead_code)] + notes: Option<String>, + #[allow(dead_code)] + text: Option<Value>, + #[allow(dead_code)] + file: Option<Value>, + #[allow(dead_code)] + file_length: Option<NumberOrString>, + #[allow(dead_code)] + id: Option<String>, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[get("/sends")] fn get_sends(_headers: Headers) -> Json<Value> { Json(json!({ - "Data": Vec::<Value>::new(), - "Object": "list", - "ContinuationToken": null + "data": Vec::<Value>::new(), + "object": "list", + "continuationToken": null })) } const SENDS_DISABLED_MSG: &str = "Sends are disabled."; @@ -63,14 +79,14 @@ fn get_send(uuid: &str, _headers: Headers) -> Error { #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/sends", data = "<data>")] -fn post_send(data: JsonUpcase<SendData>, _headers: Headers) -> Error { +fn post_send(data: Json<SendData>, _headers: Headers) -> Error { Error::new(SENDS_DISABLED_MSG, SENDS_DISABLED_MSG) } #[allow(dead_code)] #[derive(FromForm)] struct UploadData<'f> { - model: Json<UpCase<SendData>>, + model: Json<SendData>, data: TempFile<'f>, } @@ -88,7 +104,7 @@ fn post_send_file(data: Form<UploadData<'_>>, _headers: Headers) -> Error { #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/sends/file/v2", data = "<data>")] -fn post_send_file_v2(data: JsonUpcase<SendData>, _headers: Headers) -> Error { +fn post_send_file_v2(data: Json<SendData>, _headers: Headers) -> Error { Error::new(SENDS_DISABLED_MSG, SENDS_DISABLED_MSG) } @@ -108,20 +124,21 @@ fn post_send_file_v2_data( } #[derive(Deserialize)] -#[allow(dead_code, non_snake_case)] +#[serde(rename_all = "camelCase")] struct SendAccessData { - Password: Option<String>, + #[allow(dead_code)] + password: Option<String>, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/sends/access/<access_id>", data = "<data>")] -fn post_access(access_id: &str, data: JsonUpcase<SendAccessData>) -> Error { +fn post_access(access_id: &str, data: Json<SendAccessData>) -> Error { Error::new(SENDS_DISABLED_MSG, SENDS_DISABLED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")] -fn post_access_file(send_id: &str, file_id: &str, data: JsonUpcase<SendAccessData>) -> Error { +fn post_access_file(send_id: &str, file_id: &str, data: Json<SendAccessData>) -> Error { Error::new(SENDS_DISABLED_MSG, SENDS_DISABLED_MSG) } @@ -133,7 +150,7 @@ fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option<Na #[allow(unused_variables, clippy::needless_pass_by_value)] #[put("/sends/<id>", data = "<data>")] -fn put_send(id: &str, data: JsonUpcase<SendData>, _headers: Headers) -> Error { +fn put_send(id: &str, data: Json<SendData>, _headers: Headers) -> Error { Error::new(SENDS_DISABLED_MSG, SENDS_DISABLED_MSG) } diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs @@ -1,13 +1,13 @@ use crate::{ - api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData}, + api::{EmptyResult, JsonResult, PasswordOrOtpData}, auth::{ClientIp, Headers}, crypto, db::{models::Totp, DbConn}, error::Error, + util::NumberOrString, }; use data_encoding::BASE32; -use rocket::serde::json::Json; -use rocket::Route; +use rocket::{serde::json::Json, Route}; pub fn routes() -> Vec<Route> { routes![ @@ -19,11 +19,11 @@ pub fn routes() -> Vec<Route> { #[post("/two-factor/get-authenticator", data = "<data>")] async fn generate_authenticator( - data: JsonUpcase<PasswordOrOtpData>, + data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: PasswordOrOtpData = data.into_inner().data; + let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; data.validate(&user)?; let totp = Totp::find_by_user(&user.uuid, &conn).await?; @@ -32,34 +32,34 @@ async fn generate_authenticator( _ => (false, crypto::encode_random_bytes::<20>(&BASE32)), }; Ok(Json(json!({ - "Enabled": enabled, - "Key": key, - "Object": "twoFactorAuthenticator" + "enabled": enabled, + "key": key, + "object": "twoFactorAuthenticator" }))) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct EnableAuthenticatorData { - Key: String, - Token: NumberOrString, - MasterPasswordHash: Option<String>, - Otp: Option<String>, + key: String, + token: NumberOrString, + master_password_hash: Option<String>, + otp: Option<String>, } #[post("/two-factor/authenticator", data = "<data>")] async fn activate_authenticator( - data: JsonUpcase<EnableAuthenticatorData>, + data: Json<EnableAuthenticatorData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: EnableAuthenticatorData = data.into_inner().data; - let key = data.Key; - let token = data.Token.into_string(); + let data: EnableAuthenticatorData = data.into_inner(); + let key = data.key; + let token = data.token.into_string(); let user = headers.user; PasswordOrOtpData { - MasterPasswordHash: data.MasterPasswordHash, - Otp: data.Otp, + master_password_hash: data.master_password_hash, + otp: data.otp, } .validate(&user)?; // Validate key as base32 and 20 bytes length @@ -73,15 +73,15 @@ async fn activate_authenticator( // Validate the token provided with the key, and save new twofactor validate_totp_code(user.uuid, &token, key.to_uppercase(), &headers.ip, &conn).await?; Ok(Json(json!({ - "Enabled": true, - "Key": key, - "Object": "twoFactorAuthenticator" + "enabled": true, + "key": key, + "object": "twoFactorAuthenticator" }))) } #[put("/two-factor/authenticator", data = "<data>")] async fn activate_authenticator_put( - data: JsonUpcase<EnableAuthenticatorData>, + data: Json<EnableAuthenticatorData>, headers: Headers, conn: DbConn, ) -> JsonResult { diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs @@ -1,9 +1,5 @@ -use crate::{ - api::{JsonUpcase, PasswordOrOtpData}, - auth::Headers, - error::Error, -}; -use rocket::Route; +use crate::{api::PasswordOrOtpData, auth::Headers, error::Error}; +use rocket::{serde::json::Json, Route}; pub fn routes() -> Vec<Route> { routes![activate_duo, activate_duo_put, get_duo] @@ -11,28 +7,33 @@ pub fn routes() -> Vec<Route> { const DUO_DISABLED_MSG: &str = "Duo is disabled."; #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/get-duo", data = "<data>")] -fn get_duo(data: JsonUpcase<PasswordOrOtpData>, _headers: Headers) -> Error { +fn get_duo(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { Error::new(DUO_DISABLED_MSG, DUO_DISABLED_MSG) } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct EnableDuoData { - Host: String, - SecretKey: String, - IntegrationKey: String, - MasterPasswordHash: Option<String>, - Otp: Option<String>, + #[allow(dead_code)] + host: String, + #[allow(dead_code)] + secret_key: String, + #[allow(dead_code)] + integration_key: String, + #[allow(dead_code)] + master_password_hash: Option<String>, + #[allow(dead_code)] + otp: Option<String>, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/duo", data = "<data>")] -fn activate_duo(data: JsonUpcase<EnableDuoData>, _headers: Headers) -> Error { +fn activate_duo(data: Json<EnableDuoData>, _headers: Headers) -> Error { Error::new(DUO_DISABLED_MSG, DUO_DISABLED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[put("/two-factor/duo", data = "<data>")] -fn activate_duo_put(data: JsonUpcase<EnableDuoData>, _headers: Headers) -> Error { +fn activate_duo_put(data: Json<EnableDuoData>, _headers: Headers) -> Error { Error::new(DUO_DISABLED_MSG, DUO_DISABLED_MSG) } diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs @@ -1,57 +1,64 @@ -use crate::{ - api::{JsonUpcase, PasswordOrOtpData}, - auth::Headers, - error::Error, -}; -use rocket::Route; +use crate::{api::PasswordOrOtpData, auth::Headers, error::Error}; +use rocket::{serde::json::Json, Route}; pub fn routes() -> Vec<Route> { routes![email, get_email, send_email, send_email_login] } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct SendEmailLoginData { - Email: String, - MasterPasswordHash: String, + #[allow(dead_code)] + #[serde(alias = "Email")] + email: String, + #[allow(dead_code)] + #[serde(alias = "MasterPasswordHash")] + master_password_hash: String, } const EMAIL_DISABLED_MSG: &str = "E-mail is disabled."; #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/send-email-login", data = "<data>")] -fn send_email_login(data: JsonUpcase<SendEmailLoginData>) -> Error { +fn send_email_login(data: Json<SendEmailLoginData>) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/get-email", data = "<data>")] -fn get_email(data: JsonUpcase<PasswordOrOtpData>, _headers: Headers) -> Error { +fn get_email(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct SendEmailData { - Email: String, - MasterPasswordHash: Option<String>, - Otp: Option<String>, + #[allow(dead_code)] + email: String, + #[allow(dead_code)] + master_password_hash: Option<String>, + #[allow(dead_code)] + otp: Option<String>, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/send-email", data = "<data>")] -fn send_email(data: JsonUpcase<SendEmailData>, _headers: Headers) -> Error { +fn send_email(data: Json<SendEmailData>, _headers: Headers) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct EmailData { - Email: String, - Token: String, - MasterPasswordHash: Option<String>, - Otp: Option<String>, + #[allow(dead_code)] + email: String, + #[allow(dead_code)] + token: String, + #[allow(dead_code)] + master_password_hash: Option<String>, + #[allow(dead_code)] + otp: Option<String>, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[put("/two-factor/email", data = "<data>")] -fn email(data: JsonUpcase<EmailData>, _headers: Headers) -> Error { +fn email(data: Json<EmailData>, _headers: Headers) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) } diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs @@ -1,10 +1,11 @@ use crate::{ - api::{JsonResult, JsonUpcase, NumberOrString, PasswordOrOtpData}, + api::{JsonResult, PasswordOrOtpData}, auth::{ClientHeaders, Headers}, db::{ models::{OrgPolicyType, TwoFactorType, UserOrgType, UserOrganization}, DbConn, }, + util::NumberOrString, }; pub mod authenticator; mod duo; @@ -37,55 +38,58 @@ pub fn routes() -> Vec<Route> { #[get("/two-factor")] async fn get_twofactor(headers: Headers, conn: DbConn) -> Json<Value> { Json(json!({ - "Data": TwoFactorType::get_factors(&headers.user.uuid, &conn).await.expect("unable to get two factors"), - "Object": "list", - "ContinuationToken": null, + "data": TwoFactorType::get_factors(&headers.user.uuid, &conn).await.expect("unable to get two factors"), + "object": "list", + "continuationToken": null, })) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/get-recover", data = "<data>")] -fn get_recover(data: JsonUpcase<PasswordOrOtpData>, _headers: Headers) -> JsonResult { +fn get_recover(data: Json<PasswordOrOtpData>, _headers: Headers) -> JsonResult { err!("recovery codes are disabled") } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct RecoverTwoFactor { - MasterPasswordHash: String, - Email: String, - RecoveryCode: String, + #[allow(dead_code)] + master_password_hash: String, + #[allow(dead_code)] + email: String, + #[allow(dead_code)] + recovery_code: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/recover", data = "<data>")] -fn recover(data: JsonUpcase<RecoverTwoFactor>, _client_headers: ClientHeaders) -> JsonResult { +fn recover(data: Json<RecoverTwoFactor>, _client_headers: ClientHeaders) -> JsonResult { err!("recovery codes are disabled") } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct DisableTwoFactorData { - MasterPasswordHash: Option<String>, - Otp: Option<String>, - Type: NumberOrString, + master_password_hash: Option<String>, + otp: Option<String>, + r#type: NumberOrString, } #[post("/two-factor/disable", data = "<data>")] async fn disable_twofactor( - data: JsonUpcase<DisableTwoFactorData>, + data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: DisableTwoFactorData = data.into_inner().data; + let data: DisableTwoFactorData = data.into_inner(); let user = headers.user; // Delete directly after a valid token has been provided PasswordOrOtpData { - MasterPasswordHash: data.MasterPasswordHash, - Otp: data.Otp, + master_password_hash: data.master_password_hash, + otp: data.otp, } .validate(&user)?; - let type_ = data.Type.into_i32()?; + let type_ = data.r#type.into_i32()?; TwoFactorType::try_from(type_)? .delete_by_user(&user.uuid, &conn) .await?; @@ -103,15 +107,15 @@ async fn disable_twofactor( } } Ok(Json(json!({ - "Enabled": false, - "Type": type_, - "Object": "twoFactorProvider" + "enabled": false, + "type": type_, + "object": "twoFactorProvider" }))) } #[put("/two-factor/disable", data = "<data>")] async fn disable_twofactor_put( - data: JsonUpcase<DisableTwoFactorData>, + data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn, ) -> JsonResult { diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs @@ -1,5 +1,5 @@ -use crate::{api::JsonUpcase, auth::Headers, error::Error}; -use rocket::Route; +use crate::{auth::Headers, error::Error}; +use rocket::{serde::json::Json, Route}; pub fn routes() -> Vec<Route> { routes![request_otp, verify_otp] @@ -12,13 +12,13 @@ fn request_otp(_headers: Headers) -> Error { } #[derive(Deserialize, Serialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ProtectedActionVerify { - OTP: String, + otp: String, } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/verify-otp", data = "<data>")] -fn verify_otp(data: JsonUpcase<ProtectedActionVerify>, _headers: Headers) -> Error { +fn verify_otp(data: Json<ProtectedActionVerify>, _headers: Headers) -> Error { Error::new(DEVICE_LOG_IN_MSG, DEVICE_LOG_IN_MSG) } diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs @@ -1,5 +1,5 @@ use crate::{ - api::{EmptyResult, JsonResult, JsonUpcase, PasswordOrOtpData}, + api::{EmptyResult, JsonResult, PasswordOrOtpData}, auth::Headers, config, db::{ @@ -34,28 +34,24 @@ fn build_webauthn() -> Result<Webauthn, WebauthnError> { } #[post("/two-factor/get-webauthn", data = "<data>")] -async fn get_webauthn( - data: JsonUpcase<PasswordOrOtpData>, - headers: Headers, - conn: DbConn, -) -> JsonResult { - let data: PasswordOrOtpData = data.into_inner().data; +async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult { + let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; data.validate(&user)?; let keys = WebAuthnInfo::get_all_by_user(&user.uuid, &conn).await?; Ok(Json(json!({ - "Enabled": !keys.is_empty(), - "Keys": keys, - "Object": "twoFactorWebAuthn" + "enabled": !keys.is_empty(), + "keys": keys, + "object": "twoFactorWebAuthn" }))) } #[post("/two-factor/get-webauthn-challenge", data = "<data>")] async fn generate_webauthn_challenge( - data: JsonUpcase<PasswordOrOtpData>, + data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: PasswordOrOtpData = data.into_inner().data; + let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; data.validate(&user)?; let (challenge, registration) = build_webauthn()?.start_securitykey_registration( @@ -93,8 +89,8 @@ async fn activate_webauthn( let data = data.into_inner(); let user = headers.user; PasswordOrOtpData { - MasterPasswordHash: Some(data.master_password_hash), - Otp: None, + master_password_hash: Some(data.master_password_hash), + otp: None, } .validate(&user)?; // Retrieve and delete the saved challenge state @@ -111,9 +107,9 @@ async fn activate_webauthn( .await?; let keys = WebAuthnInfo::get_all_by_user(user.uuid.as_str(), &conn).await?; Ok(Json(json!({ - "Enabled": !keys.is_empty(), - "Keys": keys, - "Object": "twoFactorU2f" + "enabled": !keys.is_empty(), + "keys": keys, + "object": "twoFactorWebAuthn" }))) } @@ -127,30 +123,26 @@ async fn activate_webauthn_put( } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct DeleteU2FData { - Id: i64, - MasterPasswordHash: String, + id: i64, + master_password_hash: String, } #[delete("/two-factor/webauthn", data = "<data>")] -async fn delete_webauthn( - data: JsonUpcase<DeleteU2FData>, - headers: Headers, - conn: DbConn, -) -> JsonResult { +async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbConn) -> JsonResult { if !headers .user - .check_valid_password(&data.data.MasterPasswordHash) + .check_valid_password(&data.master_password_hash) { err!("Invalid password"); } - WebAuthn::delete_by_user_uuid_and_id(&headers.user.uuid, data.data.Id, &conn).await?; + WebAuthn::delete_by_user_uuid_and_id(&headers.user.uuid, data.id, &conn).await?; let keys = WebAuthnInfo::get_all_by_user(&headers.user.uuid, &conn).await?; Ok(Json(json!({ - "Enabled": !keys.is_empty(), - "Keys": keys, - "Object": "twoFactorU2f" + "enabled": !keys.is_empty(), + "keys": keys, + "object": "twoFactorWebAuthn" }))) } diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs @@ -1,41 +1,45 @@ -use crate::{ - api::{JsonUpcase, PasswordOrOtpData}, - auth::Headers, - error::Error, -}; -use rocket::Route; +use crate::{api::PasswordOrOtpData, auth::Headers, error::Error}; +use rocket::{serde::json::Json, Route}; pub fn routes() -> Vec<Route> { routes![activate_yubikey, activate_yubikey_put, generate_yubikey] } #[derive(Deserialize)] -#[allow(non_snake_case, dead_code)] +#[serde(rename_all = "camelCase")] struct EnableYubikeyData { - Key1: Option<String>, - Key2: Option<String>, - Key3: Option<String>, - Key4: Option<String>, - Key5: Option<String>, - Nfc: bool, - MasterPasswordHash: Option<String>, - Otp: Option<String>, + #[allow(dead_code)] + key1: Option<String>, + #[allow(dead_code)] + key2: Option<String>, + #[allow(dead_code)] + key3: Option<String>, + #[allow(dead_code)] + key4: Option<String>, + #[allow(dead_code)] + key5: Option<String>, + #[allow(dead_code)] + nfc: bool, + #[allow(dead_code)] + master_password_hash: Option<String>, + #[allow(dead_code)] + otp: Option<String>, } const YUBI_DISABLED_MSG: &str = "Yubico OTP is disabled."; #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/get-yubikey", data = "<data>")] -fn generate_yubikey(data: JsonUpcase<PasswordOrOtpData>, _headers: Headers) -> Error { +fn generate_yubikey(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { Error::new(YUBI_DISABLED_MSG, YUBI_DISABLED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/two-factor/yubikey", data = "<data>")] -fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, _headers: Headers) -> Error { +fn activate_yubikey(data: Json<EnableYubikeyData>, _headers: Headers) -> Error { Error::new(YUBI_DISABLED_MSG, YUBI_DISABLED_MSG) } #[allow(unused_variables, clippy::needless_pass_by_value)] #[put("/two-factor/yubikey", data = "<data>")] -fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, _headers: Headers) -> Error { +fn activate_yubikey_put(data: Json<EnableYubikeyData>, _headers: Headers) -> Error { Error::new(YUBI_DISABLED_MSG, YUBI_DISABLED_MSG) } diff --git a/src/api/identity.rs b/src/api/identity.rs @@ -1,7 +1,7 @@ use crate::{ api::{ core::accounts::{PreloginData, RegisterData, _prelogin}, - ApiResult, EmptyResult, JsonResult, JsonUpcase, + ApiResult, EmptyResult, JsonResult, }, auth::{ClientHeaders, ClientIp}, config, @@ -71,15 +71,22 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn) -> JsonResult { "expires_in": expires_in, "token_type": "Bearer", "refresh_token": device.refresh_token, - "Key": user.akey, + "scope": scope, "PrivateKey": user.private_key, + "Key": user.akey, + "MasterPasswordPolicy": { + "Object": "masterPasswordPolicy" + }, + "ForcePasswordReset": false, + "ResetMasterPassword": false, "Kdf": user.client_kdf_type, "KdfIterations": user.client_kdf_iter(), "KdfMemory": user.client_kdf_memory(), "KdfParallelism": user.client_kdf_parallelism(), - "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing - "scope": scope, - "unofficialServer": true, + "UserDecryptionOptions": { + "HasMasterPassword": true, + "Object": "userDecryptionOptions" + }, }); Ok(Json(result)) } @@ -160,7 +167,11 @@ async fn _password_login( "KdfIterations": kdf_iter, "KdfMemory": kdf_mem, "KdfParallelism": kdf_par, - "ResetMasterPassword": false,// TODO: Same as above + "ResetMasterPassword": false, // TODO: Same as above + "ForcePasswordReset": false, + "MasterPasswordPolicy": { + "object": "masterPasswordPolicy", + }, "scope": scope, "unofficialServer": true, "UserDecryptionOptions": { @@ -280,7 +291,10 @@ async fn _json_err_twofactor( "error" : "invalid_grant", "error_description" : "Two factor required.", "TwoFactorProviders" : if authn { if totp { providers.as_slice() } else { &providers[..1] } } else if totp { &providers[1..] } else { [].as_slice() }, - "TwoFactorProviders2" : {} // { "0" : null } + "TwoFactorProviders2" : {}, + "MasterPasswordPolicy": { + "Object": "masterPasswordPolicy" + } }); if authn { let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn).await?; @@ -293,13 +307,13 @@ async fn _json_err_twofactor( } #[post("/accounts/prelogin", data = "<data>")] -async fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> { +async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> { _prelogin(data, conn).await } #[allow(unused_variables, clippy::needless_pass_by_value)] #[post("/accounts/register", data = "<data>")] -fn identity_register(data: JsonUpcase<RegisterData>) -> Error { +fn identity_register(data: Json<RegisterData>) -> Error { const MSG: &str = "No more registerations allowed."; Error::new(MSG, MSG) } @@ -307,7 +321,6 @@ fn identity_register(data: JsonUpcase<RegisterData>) -> Error { // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/tokenRequest.ts // https://github.com/bitwarden/mobile/blob/master/src/Core/Models/Request/TokenRequest.cs #[derive(Clone, Default, FromForm)] -#[allow(non_snake_case)] struct ConnectData { #[field(name = uncased("grant_type"))] #[field(name = uncased("granttype"))] diff --git a/src/api/mod.rs b/src/api/mod.rs @@ -11,23 +11,19 @@ pub use crate::api::{ }; use crate::db::models::User; use crate::error::Error; -use crate::util; use rocket::serde::json::Json; use serde_json::Value; // Type aliases for API methods results type ApiResult<T> = Result<T, Error>; type JsonResult = ApiResult<Json<Value>>; pub type EmptyResult = ApiResult<()>; -type JsonUpcase<T> = Json<util::UpCase<T>>; -type JsonUpcaseVec<T> = Json<Vec<util::UpCase<T>>>; -type JsonVec<T> = Json<Vec<T>>; // Common structs representing JSON data received #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct PasswordOrOtpData { - MasterPasswordHash: Option<String>, - Otp: Option<String>, + master_password_hash: Option<String>, + otp: Option<String>, } impl PasswordOrOtpData { @@ -35,7 +31,7 @@ impl PasswordOrOtpData { /// First for the validation to continue, after that to enable or validate the following actions /// This is different per caller, so it can be adjusted to delete the token or not fn validate(&self, user: &User) -> EmptyResult { - match (self.MasterPasswordHash.as_deref(), self.Otp.as_deref()) { + match (self.master_password_hash.as_deref(), self.otp.as_deref()) { (Some(pw_hash), None) => { if !user.check_valid_password(pw_hash) { err!("Invalid password"); @@ -49,29 +45,3 @@ impl PasswordOrOtpData { Ok(()) } } - -#[derive(Deserialize, Clone)] -#[serde(untagged)] -enum NumberOrString { - Number(i32), - String(String), -} - -impl NumberOrString { - fn into_string(self) -> String { - match self { - Self::Number(n) => n.to_string(), - Self::String(s) => s, - } - } - #[allow(clippy::wrong_self_convention)] - fn into_i32(&self) -> ApiResult<i32> { - use std::num::ParseIntError as PIE; - match *self { - Self::Number(n) => Ok(n), - Self::String(ref s) => s - .parse() - .map_err(|e: PIE| Error::new("Can't convert to number", e.to_string())), - } - } -} diff --git a/src/api/web.rs b/src/api/web.rs @@ -135,9 +135,9 @@ fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> { ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap.bundle.js"), )), - "jdenticon.js" => Ok(( + "jdenticon-3.3.0.js" => Ok(( ContentType::JavaScript, - include_bytes!("../static/scripts/jdenticon.js"), + include_bytes!("../static/scripts/jdenticon-3.3.0.js"), )), "datatables.js" => Ok(( ContentType::JavaScript, @@ -147,9 +147,9 @@ fn static_files(filename: &str) -> Result<(ContentType, &'static [u8]), Error> { ContentType::CSS, include_bytes!("../static/scripts/datatables.css"), )), - "jquery-3.7.0.slim.js" => Ok(( + "jquery-3.7.1.slim.js" => Ok(( ContentType::JavaScript, - include_bytes!("../static/scripts/jquery-3.7.0.slim.js"), + include_bytes!("../static/scripts/jquery-3.7.1.slim.js"), )), _ => err!(format!("Static file not found: {filename}")), } diff --git a/src/auth.rs b/src/auth.rs @@ -2,7 +2,7 @@ use crate::{ config::{self, Config}, error::Error, }; -use chrono::{Duration, Utc}; +use chrono::{TimeDelta, Utc}; use jsonwebtoken::{self, errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header}; use openssl::pkey::{Id, PKey}; use serde::de::DeserializeOwned; @@ -11,15 +11,15 @@ use std::fs::File; use std::io::{Read, Write}; use std::sync::OnceLock; -static DEFAULT_VALIDITY: OnceLock<Duration> = OnceLock::new(); +static DEFAULT_VALIDITY: OnceLock<TimeDelta> = OnceLock::new(); #[inline] fn init_default_validity() { DEFAULT_VALIDITY - .set(Duration::hours(2)) + .set(TimeDelta::try_hours(2).expect("TimeDelta::try_hours(2) should work")) .expect("DEFAULT_VALIDITY must only be initialized once"); } #[inline] -pub fn get_default_validity() -> &'static Duration { +pub fn get_default_validity() -> &'static TimeDelta { DEFAULT_VALIDITY .get() .expect("DEFAULT_VALIDITY must be initialized in main") @@ -291,8 +291,6 @@ impl<'r> FromRequest<'r> for Host { } pub struct ClientHeaders { #[allow(dead_code)] - pub host: String, - #[allow(dead_code)] pub device_type: i32, pub ip: ClientIp, } @@ -301,7 +299,6 @@ pub struct ClientHeaders { impl<'r> FromRequest<'r> for ClientHeaders { type Error = &'static str; async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { - let host = try_outcome!(Host::from_request(request).await).host; let Outcome::Success(ip) = ClientIp::from_request(request).await else { err_handler!("Error getting Client IP") }; @@ -311,11 +308,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { .get_one("device-type") .map_or(14i32, |d| d.parse().unwrap_or(14i32)); - Outcome::Success(Self { - host, - device_type, - ip, - }) + Outcome::Success(Self { device_type, ip }) } } @@ -408,8 +401,6 @@ pub struct OrgHeaders { user: User, org_user_type: UserOrgType, org_user: UserOrganization, - #[allow(dead_code)] - pub org_id: String, ip: ClientIp, } @@ -472,7 +463,6 @@ impl<'r> FromRequest<'r> for OrgHeaders { } }, org_user, - org_id: String::from(org_id), ip: headers.ip, }) } @@ -549,8 +539,6 @@ pub struct ManagerHeaders { host: String, device: Device, pub user: User, - #[allow(dead_code)] - pub org_user_type: UserOrgType, ip: ClientIp, } @@ -566,7 +554,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders { err_handler!("Error getting DB") }; - if !can_access_collection(&headers.org_user, &col_id, &conn).await { + if !Collection::can_access_collection(&headers.org_user, &col_id, &conn).await { err_handler!("The current user isn't a manager for this collection") } } @@ -577,7 +565,6 @@ impl<'r> FromRequest<'r> for ManagerHeaders { host: headers.host, device: headers.device, user: headers.user, - org_user_type: headers.org_user_type, ip: headers.ip, }) } else { @@ -604,7 +591,6 @@ pub struct ManagerHeadersLoose { device: Device, pub user: User, pub org_user: UserOrganization, - org_user_type: UserOrgType, ip: ClientIp, } @@ -619,7 +605,6 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose { device: headers.device, user: headers.user, org_user: headers.org_user, - org_user_type: headers.org_user_type, ip: headers.ip, }) } else { @@ -638,11 +623,6 @@ impl From<ManagerHeadersLoose> for Headers { } } } -async fn can_access_collection(org_user: &UserOrganization, col_id: &str, conn: &DbConn) -> bool { - org_user.has_full_access() - || Collection::has_access_by_collection_and_user_uuid(col_id, &org_user.user_uuid, conn) - .await -} impl ManagerHeaders { pub async fn from_loose( @@ -654,7 +634,7 @@ impl ManagerHeaders { if uuid::Uuid::parse_str(col_id).is_err() { err!("Collection Id is malformed!"); } - if !can_access_collection(&h.org_user, col_id, conn).await { + if !Collection::can_access_collection(&h.org_user, col_id, conn).await { err!("You don't have access to all collections!"); } } @@ -662,7 +642,6 @@ impl ManagerHeaders { host: h.host, device: h.device, user: h.user, - org_user_type: h.org_user_type, ip: h.ip, }) } @@ -670,8 +649,6 @@ impl ManagerHeaders { pub struct OwnerHeaders { #[allow(dead_code)] - pub host: String, - #[allow(dead_code)] pub device: Device, pub user: User, #[allow(dead_code)] @@ -685,7 +662,6 @@ impl<'r> FromRequest<'r> for OwnerHeaders { let headers = try_outcome!(OrgHeaders::from_request(request).await); if headers.org_user_type == UserOrgType::Owner { Outcome::Success(Self { - host: headers.host, device: headers.device, user: headers.user, ip: headers.ip, diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs @@ -66,7 +66,7 @@ impl Cipher { pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult { let mut validation_errors = serde_json::Map::new(); for (index, cipher) in cipher_data.iter().enumerate() { - if let Some(ref note) = cipher.Notes { + if let Some(ref note) = cipher.notes { if note.len() > 10_000 { validation_errors.insert( format!("Ciphers[{index}].Notes"), @@ -139,21 +139,28 @@ impl Cipher { type_data_json["Uri"] = Value::Null; } } + if self.atype == 2i32 + && (self.data.is_empty() + || self.data.eq("{}") + || self.data.to_ascii_lowercase().eq("{\"type\":null}")) + { + type_data_json = json!({"type": 0i32}); + } // Clone the type_data and add some default value. let mut data_json = type_data_json.clone(); // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // data_json should always contain the following keys with every atype - data_json["Fields"] = fields_json.clone(); - data_json["Name"] = json!(self.name); - data_json["Notes"] = json!(self.notes); - data_json["PasswordHistory"] = password_history_json.clone(); + data_json["fields"] = fields_json.clone(); + data_json["name"] = json!(self.name); + data_json["notes"] = json!(self.notes); + data_json["passwordHistory"] = password_history_json.clone(); let collection_ids = if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data .cipher_collections .get(&self.uuid) .map_or_else(|| Cow::from(Vec::with_capacity(0)), Cow::from) } else { - Cow::from(self.get_collections(user_uuid.to_owned(), conn).await) + Cow::from(self.get_admin_collections(user_uuid.to_owned(), conn).await) }; // There are three types of cipher response models in upstream // Bitwarden: "cipherMini", "cipher", and "cipherDetails" (in order @@ -163,37 +170,37 @@ impl Cipher { // // Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs let mut json_object = json!({ - "Object": "cipherDetails", - "Id": self.uuid, - "Type": self.atype, - "CreationDate": util::format_date(&self.created_at), - "RevisionDate": util::format_date(&self.updated_at), - "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(util::format_date(&d))), - "Reprompt": self.reprompt.unwrap_or_else(|| i32::from(RepromptType::None)), - "OrganizationId": self.organization_uuid, - "Key": self.key, - "Attachments": Value::Null, + "object": "cipherDetails", + "id": self.uuid, + "type": self.atype, + "creationDate": util::format_date(&self.created_at), + "revisionDate": util::format_date(&self.updated_at), + "deletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(util::format_date(&d))), + "reprompt": self.reprompt.unwrap_or_else(|| i32::from(RepromptType::None)), + "organizationId": self.organization_uuid, + "key": self.key, + "attachments": Value::Null, // We have UseTotp set to true by default within the Organization model. // This variable together with UsersGetPremium is used to show or hide the TOTP counter. - "OrganizationUseTotp": true, + "organizationUseTotp": true, // This field is specific to the cipherDetails type. - "CollectionIds": collection_ids, - "Name": self.name, - "Notes": self.notes, - "Fields": fields_json, - "Data": data_json, - "PasswordHistory": password_history_json, + "collectionIds": collection_ids, + "name": self.name, + "notes": self.notes, + "fields": fields_json, + "data": data_json, + "passwordHistory": password_history_json, // All Cipher types are included by default as null, but only the matching one will be populated - "Login": null, - "SecureNote": null, - "Card": null, - "Identity": null, + "login": null, + "secureNote": null, + "card": null, + "identity": null, }); // These values are only needed for user/default syncs // Not during an organizational sync like `get_org_details` // Skip adding these fields in that case if sync_type == CipherSyncType::User { - json_object["FolderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { + json_object["folderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data .cipher_folders .get(&self.uuid) @@ -201,7 +208,7 @@ impl Cipher { } else { self.get_folder_uuid(user_uuid, conn).await }); - json_object["Favorite"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { + json_object["favorite"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_favorites.contains(&self.uuid) } else { self.is_favorite(user_uuid, conn).await @@ -209,15 +216,15 @@ impl Cipher { // These values are true by default, but can be false if the // cipher belongs to a collection or group where the org owner has enabled // the "Read Only" or "Hide Passwords" restrictions for the user. - json_object["Edit"] = json!(!read_only); - json_object["ViewPassword"] = json!(!hide_passwords); + json_object["edit"] = json!(!read_only); + json_object["viewPassword"] = json!(!hide_passwords); } let key = match self.atype { - 1i32 => "Login", - 2i32 => "SecureNote", - 3i32 => "Card", - 4i32 => "Identity", + 1i32 => "login", + 2i32 => "secureNote", + 3i32 => "card", + 4i32 => "identity", _ => panic!("Wrong type"), }; @@ -490,6 +497,21 @@ impl Cipher { }} } + pub async fn find_by_uuid_and_org( + cipher_uuid: &str, + org_uuid: &str, + conn: &DbConn, + ) -> Option<Self> { + db_run! {conn: { + ciphers::table + .filter(ciphers::uuid.eq(cipher_uuid)) + .filter(ciphers::organization_uuid.eq(org_uuid)) + .first::<CipherDb>(conn) + .ok() + .from_db() + }} + } + // Find all ciphers accessible or visible to the specified user. // // "Accessible" means the user has read access to the cipher, either via @@ -606,27 +628,49 @@ impl Cipher { pub async fn get_collections(&self, user_id: String, conn: &DbConn) -> Vec<String> { db_run! {conn: { ciphers_collections::table - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid).and( - users_organizations::user_uuid.eq(user_id.clone()) - ) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( - users_collections::user_uuid.eq(user_id.clone()) + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_id.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_id.clone())) + )) + .filter(users_organizations::access_all.eq(true) // User has access all + .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .and(users_collections::read_only.eq(false))) ) - )) - .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) - .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection - users_organizations::access_all.eq(true).or( // User has access all - users_organizations::atype.le(i32::from(UserOrgType::Admin)) // User is admin or owner + .select(ciphers_collections::collection_uuid) + .load::<String>(conn).unwrap_or_default() + }} + } + + pub async fn get_admin_collections(&self, user_id: String, conn: &DbConn) -> Vec<String> { + db_run! {conn: { + ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_id.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_id.clone())) + )) + .filter(users_organizations::access_all.eq(true) // User has access all + .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .and(users_collections::read_only.eq(false))) + .or(users_organizations::atype.le(i32::from(UserOrgType::Admin))) // User is admin or owner ) - )) - .select(ciphers_collections::collection_uuid) - .load::<String>(conn).unwrap_or_default() + .select(ciphers_collections::collection_uuid) + .load::<String>(conn).unwrap_or_default() }} } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs @@ -46,11 +46,11 @@ impl Collection { pub fn to_json(&self) -> Value { json!({ - "ExternalId": self.external_id, - "Id": self.uuid, - "OrganizationId": self.org_uuid, - "Name": self.name, - "Object": "collection", + "externalId": self.external_id, + "id": self.uuid, + "organizationId": self.org_uuid, + "name": self.name, + "object": "collection", }) } @@ -91,11 +91,26 @@ impl Collection { ) }; let mut json_object = self.to_json(); - json_object["Object"] = json!("collectionDetails"); - json_object["ReadOnly"] = json!(read_only); - json_object["HidePasswords"] = json!(hide_passwords); + json_object["object"] = json!("collectionDetails"); + json_object["readOnly"] = json!(read_only); + json_object["hidePasswords"] = json!(hide_passwords); json_object } + + pub async fn can_access_collection( + org_user: &UserOrganization, + col_id: &str, + conn: &DbConn, + ) -> bool { + org_user.has_status(UserOrgStatus::Confirmed) + && (org_user.has_full_access() + || CollectionUser::has_access_to_collection_by_user( + col_id, + &org_user.user_uuid, + conn, + ) + .await) + } } use crate::api::EmptyResult; @@ -191,20 +206,6 @@ impl Collection { }} } - // Check if a user has access to a specific collection - // FIXME: This needs to be reviewed. The query used by `find_by_user_uuid` could be adjusted to filter when needed. - // For now this is a good solution without making too much changes. - pub async fn has_access_by_collection_and_user_uuid( - collection_uuid: &str, - user_uuid: &str, - conn: &DbConn, - ) -> bool { - Self::find_by_user_uuid(user_uuid.to_owned(), conn) - .await - .into_iter() - .any(|c| c.uuid == collection_uuid) - } - pub async fn find_by_organization_and_user_uuid( org_uuid: &str, user_uuid: &str, @@ -285,27 +286,24 @@ impl Collection { let user_uuid = user_uuid.to_owned(); db_run! { conn: { collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) - ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) - ) - )) - .filter(collections::uuid.eq(&self.uuid)) - .filter( - users_collections::collection_uuid.eq(&self.uuid).and(users_collections::read_only.eq(false)).or(// Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(i32::from(UserOrgType::Admin)) // Org admin or owner + .filter(collections::uuid.eq(&self.uuid)) + .inner_join(users_organizations::table.on( + collections::org_uuid.eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone())) )) - ) - .count() - .first::<i64>(conn) - .ok() - .unwrap_or(0) != 0 + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid)) + )) + .filter(users_organizations::atype.le(i32::from(UserOrgType::Admin)) // Org admin or owner + .or(users_organizations::access_all.eq(true)) // access_all via membership + .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection + .and(users_collections::read_only.eq(false))) + ) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) != 0 }} } @@ -506,6 +504,16 @@ impl CollectionUser { Ok(()) }} } + + pub async fn has_access_to_collection_by_user( + col_id: &str, + user_uuid: &str, + conn: &DbConn, + ) -> bool { + Self::find_by_collection_and_user(col_id, user_uuid, conn) + .await + .is_some() + } } /// Database methods diff --git a/src/db/models/device.rs b/src/db/models/device.rs @@ -49,8 +49,8 @@ impl Device { self.refresh_token = crypto::encode_random_bytes::<64>(&BASE64URL); } // Update the expiration of the device and the last update date - let time_now = Utc::now().naive_utc(); - self.updated_at = time_now; + let time_now = Utc::now(); + self.updated_at = time_now.naive_utc(); // --- // Disabled these keys to be added to the JWT since they could cause the JWT to get too large // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients @@ -65,10 +65,10 @@ impl Device { // Create the JWT claims struct, to send to the client use crate::auth::{self, encode_jwt, LoginJwtClaims}; let claims = LoginJwtClaims { - nbf: time_now.and_utc().timestamp(), - exp: (time_now.checked_add_signed(*auth::get_default_validity())) - .expect("Duration add overflowed") - .and_utc() + nbf: time_now.timestamp(), + exp: time_now + .checked_add_signed(*auth::get_default_validity()) + .expect("overflow") .timestamp(), iss: auth::get_jwt_login_issuer().to_owned(), sub: user.uuid.clone(), diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs @@ -3,7 +3,7 @@ use crate::api::EmptyResult; use crate::db::models::TwoFactorType; use crate::db::DbConn; use crate::error::{Error, MapResult}; -use crate::util::{self, UpCase}; +use crate::util; use diesel::result::{self, DatabaseErrorKind}; use serde::Deserialize; use serde_json::Value; @@ -68,9 +68,9 @@ impl TryFrom<i32> for OrgPolicyType { // https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs #[derive(Deserialize)] -#[allow(non_snake_case)] +#[serde(rename_all = "camelCase")] struct ResetPasswordDataModel { - AutoEnrollEnabled: bool, + auto_enroll_enabled: bool, } type OrgPolicyResult = Result<(), OrgPolicyErr>; pub enum OrgPolicyErr { @@ -93,12 +93,12 @@ impl OrgPolicy { pub fn to_json(&self) -> Value { let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null); json!({ - "Id": self.uuid, - "OrganizationId": self.org_uuid, - "Type": self.atype, - "Data": data_json, - "Enabled": self.enabled, - "Object": "policy", + "id": self.uuid, + "organizationId": self.org_uuid, + "type": self.atype, + "data": data_json, + "enabled": self.enabled, + "object": "policy", }) } } @@ -273,10 +273,8 @@ impl OrgPolicy { pub async fn org_is_reset_password_auto_enroll(org_uuid: &str, conn: &DbConn) -> bool { match Self::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await { Some(policy) => { - if let Ok(opts) = - serde_json::from_str::<UpCase<ResetPasswordDataModel>>(&policy.data) - { - return policy.enabled && opts.data.AutoEnrollEnabled; + if let Ok(opts) = serde_json::from_str::<ResetPasswordDataModel>(&policy.data) { + return policy.enabled && opts.auto_enroll_enabled; } } None => return false, diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -191,34 +191,35 @@ impl Organization { // https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs pub fn to_json(&self) -> Value { json!({ - "Id": self.uuid, - "Identifier": null, // not supported by us - "Name": self.name, - "Seats": 10u32, // The value doesn't matter, we don't check server-side - "MaxCollections": 10u32, // The value doesn't matter, we don't check server-side - "MaxStorageGb": 10u32, // The value doesn't matter, we don't check server-side - "Use2fa": true, - "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) - "UseEvents": false, - "UseGroups": false, - "UseTotp": true, - "UsePolicies": true, - "UseSso": false, // Not supported - "SelfHost": true, - "UseApi": true, - "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), - "UseResetPassword": false, - "BusinessName": null, - "BusinessAddress1": null, - "BusinessAddress2": null, - "BusinessAddress3": null, - "BusinessCountry": null, - "BusinessTaxNumber": null, - "BillingEmail": self.billing_email, - "Plan": "TeamsAnnually", - "PlanType": 5u32, // TeamsAnnually plan - "UsersGetPremium": true, - "Object": "organization", + "id": self.uuid, + "identifier": null, // not supported by us + "name": self.name, + "seats": null, + "maxAutoscaleSeats": null, + "maxCollections": null, + "maxStorageGb": i16::MAX, + "use2fa": true, + "useCustomPermissions": false, + "useDirectory": false, // Is supported, but this value isn't checked anywhere (yet) + "useEvents": false, + "useGroups": false, + "useTotp": true, + "usePolicies": true, + "useSso": false, // Not supported + "selfHost": true, + "useApi": true, + "hasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), + "useResetPassword": false, + "businessName": null, + "businessAddress1": null, + "businessAddress2": null, + "businessAddress3": null, + "businessCountry": null, + "businessTaxNumber": null, + "billingEmail": self.billing_email, + "planType": 6, + "usersGetPremium": true, + "object": "organization", }) } } @@ -340,36 +341,77 @@ impl UserOrganization { let org = Organization::find_by_uuid(&self.org_uuid, conn) .await .unwrap(); + let permissions = json!({ + // TODO: Add support for Custom User Roles + // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role + "accessEventLogs": false, + "accessImportExport": false, + "accessReports": false, + "createNewCollections": false, + "editAnyCollection": false, + "deleteAnyCollection": false, + "editAssignedCollections": false, + "deleteAssignedCollections": false, + "manageGroups": false, + "managePolicies": false, + "manageSso": false, // Not supported + "manageUsers": false, + "manageResetPassword": false, + "manageScim": false // Not supported (Not AGPLv3 Licensed) + }); // https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs json!({ - "Id": self.org_uuid, - "Identifier": null, // Not supported - "Name": org.name, - "Seats": 10u32, // The value doesn't matter, we don't check server-side - "MaxCollections": 10u32, // The value doesn't matter, we don't check server-side - "UsersGetPremium": true, - "Use2fa": true, - "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) - "UseEvents": false, - "UseGroups": false, - "UseTotp": true, - "UsePolicies": true, - "UseApi": true, - "SelfHost": true, - "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), - "ResetPasswordEnrolled": self.reset_password_key.is_some(), - "UseResetPassword": false, - "SsoBound": false, // Not supported - "UseSso": false, // Not supported - "ProviderId": null, - "ProviderName": null, - "MaxStorageGb": 10u32, // The value doesn't matter, we don't check server-side - "UserId": self.user_uuid, - "Key": self.akey, - "Status": self.status, - "Type": self.atype, - "Enabled": true, - "Object": "profileOrganization", + "id": self.org_uuid, + "identifier": null, // Not supported + "name": org.name, + "seats": null, + "maxAutoscaleSeats": null, + "maxCollections": null, + "usersGetPremium": true, + "use2fa": true, + "useDirectory": false, // Is supported, but this value isn't checked anywhere (yet) + "useEvents": false, + "useGroups": false, + "useTotp": true, + "useScim": false, // Not supported (Not AGPLv3 Licensed) + "usePolicies": true, + "useApi": false, + "selfHost": true, + "hasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), + "resetPasswordEnrolled": self.reset_password_key.is_some(), + "useResetPassword": false, + "ssoBound": false, // Not supported + "useSso": false, // Not supported + "useKeyConnector": false, + "useSecretsManager": false, + "usePasswordManager": true, + "useCustomPermissions": false, + "useActivateAutofillPolicy": false, + "organizationUserId": self.uuid, + "providerId": null, + "providerName": null, + "providerType": null, + "familySponsorshipFriendlyName": null, + "familySponsorshipAvailable": false, + "planProductType": 3i32, + "productTierType": 3i32, + "keyConnectorEnabled": false, + "keyConnectorUrl": null, + "familySponsorshipLastSyncDate": null, + "familySponsorshipValidUntil": null, + "familySponsorshipToDelete": null, + "accessSecretsManager": false, + "limitCollectionCreationDeletion": true, + "allowAdminAccessToAllCollectionItems": true, + "flexibleCollections": false, + "permissions": permissions, + "maxStorageGb": i16::MAX, + "userId": self.user_uuid, + "key": self.akey, + "status": self.status, + "type": self.atype, + "enabled": true, + "object": "profileOrganization", }) } pub async fn to_json_user_details(&self, include_collections: bool, conn: &DbConn) -> Value { @@ -396,9 +438,9 @@ impl UserOrganization { .iter() .map(|cu| { json!({ - "Id": cu.collection_uuid, - "ReadOnly": cu.read_only, - "HidePasswords": cu.hide_passwords, + "id": cu.collection_uuid, + "readOnly": cu.read_only, + "hidePasswords": cu.hide_passwords, }) }) .collect() @@ -407,27 +449,27 @@ impl UserOrganization { }; json!({ - "Id": self.uuid, - "UserId": self.user_uuid, - "Name": user.name, - "Email": user.email, - "ExternalId": self.external_id, - "Groups": groups, - "Collections": collections, - "Status": status, - "Type": self.atype, - "AccessAll": self.access_all, - "TwoFactorEnabled": twofactor_enabled, - "ResetPasswordEnrolled": self.reset_password_key.is_some(), - "Object": "organizationUserUserDetails", + "id": self.uuid, + "userId": self.user_uuid, + "name": user.name, + "email": user.email, + "externalId": self.external_id, + "groups": groups, + "collections": collections, + "status": status, + "type": self.atype, + "accessAll": self.access_all, + "twoFactorEnabled": twofactor_enabled, + "resetPasswordEnrolled": self.reset_password_key.is_some(), + "object": "organizationUserUserDetails", }) } pub fn to_json_user_access_restrictions(&self, col_user: &CollectionUser) -> Value { json!({ - "Id": self.uuid, - "ReadOnly": col_user.read_only, - "HidePasswords": col_user.hide_passwords, + "id": self.uuid, + "readOnly": col_user.read_only, + "hidePasswords": col_user.hide_passwords, }) } @@ -488,7 +530,7 @@ impl UserOrganization { None } - fn has_status(&self, status: UserOrgStatus) -> bool { + pub fn has_status(&self, status: UserOrgStatus) -> bool { self.status == i32::from(status) } @@ -616,6 +658,23 @@ impl UserOrganization { }} } + pub async fn find_confirmed_by_user_and_org( + user_uuid: &str, + org_uuid: &str, + conn: &DbConn, + ) -> Option<Self> { + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .filter( + users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed)) + ) + .first::<UserOrganizationDb>(conn) + .ok().from_db() + }} + } + pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { db_run! { conn: { users_organizations::table @@ -701,7 +760,6 @@ impl UserOrganization { mod tests { use super::*; #[test] - #[allow(non_snake_case)] fn partial_cmp_UserOrgType() { assert!(UserOrgType::Owner > UserOrgType::Admin); assert!(UserOrgType::Admin > UserOrgType::Manager); diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -1,7 +1,7 @@ use crate::config; use crate::crypto; use crate::util; -use chrono::{Duration, NaiveDateTime, Utc}; +use chrono::{NaiveDateTime, TimeDelta, Utc}; use diesel::result::{self, DatabaseErrorKind}; use serde_json::Value; @@ -230,12 +230,12 @@ impl User { routes: route_exception, security_stamp: self.security_stamp.clone(), expire: u64::try_from( - (Utc::now() - .naive_utc() - .checked_add_signed(Duration::minutes(2))) - .expect("Duration add overflowed") - .and_utc() - .timestamp(), + Utc::now() + .checked_add_signed( + TimeDelta::try_minutes(2).expect("TimeDelta::try_minutes(2) to work"), + ) + .expect("overflow") + .timestamp(), ) .expect("underflow"), }; @@ -273,24 +273,26 @@ impl User { UserStatus::Enabled }; json!({ - "_Status": i32::from(status), - "Id": self.uuid, - "Name": self.name, - "Email": self.email, - "EmailVerified": true, - "Premium": true, - "MasterPasswordHint": self.password_hint, - "Culture": "en-US", - "TwoFactorEnabled": twofactor_enabled, - "Key": self.akey, - "PrivateKey": self.private_key, - "SecurityStamp": self.security_stamp, - "Organizations": orgs_json, - "Providers": [], - "ProviderOrganizations": [], - "ForcePasswordReset": false, - "AvatarColor": self.avatar_color, - "Object": "profile", + "_status": i32::from(status), + "id": self.uuid, + "name": self.name, + "email": self.email, + "emailVerified": true, + "premium": true, + "premiumFromOrganization": false, + "masterPasswordHint": self.password_hint, + "culture": "en-US", + "twoFactorEnabled": twofactor_enabled, + "key": self.akey, + "privateKey": self.private_key, + "securityStamp": self.security_stamp, + "organizations": orgs_json, + "providers": [], + "providerOrganizations": [], + "forcePasswordReset": false, + "avatarColor": self.avatar_color, + "usesKeyConnector": false, + "object": "profile", }) } diff --git a/src/main.rs b/src/main.rs @@ -65,7 +65,7 @@ // The more key/value pairs there are the more recursion occurs. // We want to keep this as low as possible, but not higher then 128. // If you go above 128 it will cause rust-analyzer to fail, -#![recursion_limit = "103"] +#![recursion_limit = "206"] #[macro_use] extern crate diesel; #[macro_use] diff --git a/src/static/global_domains.json b/src/static/global_domains.json @@ -1,80 +1,80 @@ [ { - "Type": 2, - "Domains": [ + "type": 2, + "domains": [ "ameritrade.com", "tdameritrade.com" ], - "Excluded": false + "excluded": false }, { - "Type": 3, - "Domains": [ + "type": 3, + "domains": [ "bankofamerica.com", "bofa.com", "mbna.com", "usecfo.com" ], - "Excluded": false + "excluded": false }, { - "Type": 4, - "Domains": [ + "type": 4, + "domains": [ "sprint.com", "sprintpcs.com", "nextel.com" ], - "Excluded": false + "excluded": false }, { - "Type": 0, - "Domains": [ + "type": 0, + "domains": [ "youtube.com", "google.com", "gmail.com" ], - "Excluded": false + "excluded": false }, { - "Type": 1, - "Domains": [ + "type": 1, + "domains": [ "apple.com", "icloud.com" ], - "Excluded": false + "excluded": false }, { - "Type": 5, - "Domains": [ + "type": 5, + "domains": [ "wellsfargo.com", "wf.com", "wellsfargoadvisors.com" ], - "Excluded": false + "excluded": false }, { - "Type": 6, - "Domains": [ + "type": 6, + "domains": [ "mymerrill.com", "ml.com", "merrilledge.com" ], - "Excluded": false + "excluded": false }, { - "Type": 7, - "Domains": [ + "type": 7, + "domains": [ "accountonline.com", "citi.com", "citibank.com", "citicards.com", "citibankonline.com" ], - "Excluded": false + "excluded": false }, { - "Type": 8, - "Domains": [ + "type": 8, + "domains": [ "cnet.com", "cnettv.com", "com.com", @@ -83,21 +83,21 @@ "search.com", "upload.com" ], - "Excluded": false + "excluded": false }, { - "Type": 9, - "Domains": [ + "type": 9, + "domains": [ "bananarepublic.com", "gap.com", "oldnavy.com", "piperlime.com" ], - "Excluded": false + "excluded": false }, { - "Type": 10, - "Domains": [ + "type": 10, + "domains": [ "bing.com", "hotmail.com", "live.com", @@ -113,53 +113,53 @@ "azure.com", "windowsazure.com" ], - "Excluded": false + "excluded": false }, { - "Type": 11, - "Domains": [ + "type": 11, + "domains": [ "ua2go.com", "ual.com", "united.com", "unitedwifi.com" ], - "Excluded": false + "excluded": false }, { - "Type": 12, - "Domains": [ + "type": 12, + "domains": [ "overture.com", "yahoo.com" ], - "Excluded": false + "excluded": false }, { - "Type": 13, - "Domains": [ + "type": 13, + "domains": [ "zonealarm.com", "zonelabs.com" ], - "Excluded": false + "excluded": false }, { - "Type": 14, - "Domains": [ + "type": 14, + "domains": [ "paypal.com", "paypal-search.com" ], - "Excluded": false + "excluded": false }, { - "Type": 15, - "Domains": [ + "type": 15, + "domains": [ "avon.com", "youravon.com" ], - "Excluded": false + "excluded": false }, { - "Type": 16, - "Domains": [ + "type": 16, + "domains": [ "diapers.com", "soap.com", "wag.com", @@ -172,19 +172,19 @@ "look.com", "vinemarket.com" ], - "Excluded": false + "excluded": false }, { - "Type": 17, - "Domains": [ + "type": 17, + "domains": [ "1800contacts.com", "800contacts.com" ], - "Excluded": false + "excluded": false }, { - "Type": 18, - "Domains": [ + "type": 18, + "domains": [ "amazon.com", "amazon.com.be", "amazon.ae", @@ -205,240 +205,240 @@ "amazon.se", "amazon.sg" ], - "Excluded": false + "excluded": false }, { - "Type": 19, - "Domains": [ + "type": 19, + "domains": [ "cox.com", "cox.net", "coxbusiness.com" ], - "Excluded": false + "excluded": false }, { - "Type": 20, - "Domains": [ + "type": 20, + "domains": [ "mynortonaccount.com", "norton.com" ], - "Excluded": false + "excluded": false }, { - "Type": 21, - "Domains": [ + "type": 21, + "domains": [ "verizon.com", "verizon.net" ], - "Excluded": false + "excluded": false }, { - "Type": 22, - "Domains": [ + "type": 22, + "domains": [ "rakuten.com", "buy.com" ], - "Excluded": false + "excluded": false }, { - "Type": 23, - "Domains": [ + "type": 23, + "domains": [ "siriusxm.com", "sirius.com" ], - "Excluded": false + "excluded": false }, { - "Type": 24, - "Domains": [ + "type": 24, + "domains": [ "ea.com", "origin.com", "play4free.com", "tiberiumalliance.com" ], - "Excluded": false + "excluded": false }, { - "Type": 25, - "Domains": [ + "type": 25, + "domains": [ "37signals.com", "basecamp.com", "basecamphq.com", "highrisehq.com" ], - "Excluded": false + "excluded": false }, { - "Type": 26, - "Domains": [ + "type": 26, + "domains": [ "steampowered.com", "steamcommunity.com", "steamgames.com" ], - "Excluded": false + "excluded": false }, { - "Type": 27, - "Domains": [ + "type": 27, + "domains": [ "chart.io", "chartio.com" ], - "Excluded": false + "excluded": false }, { - "Type": 28, - "Domains": [ + "type": 28, + "domains": [ "gotomeeting.com", "citrixonline.com" ], - "Excluded": false + "excluded": false }, { - "Type": 29, - "Domains": [ + "type": 29, + "domains": [ "gogoair.com", "gogoinflight.com" ], - "Excluded": false + "excluded": false }, { - "Type": 30, - "Domains": [ + "type": 30, + "domains": [ "mysql.com", "oracle.com" ], - "Excluded": false + "excluded": false }, { - "Type": 31, - "Domains": [ + "type": 31, + "domains": [ "discover.com", "discovercard.com" ], - "Excluded": false + "excluded": false }, { - "Type": 32, - "Domains": [ + "type": 32, + "domains": [ "dcu.org", "dcu-online.org" ], - "Excluded": false + "excluded": false }, { - "Type": 33, - "Domains": [ + "type": 33, + "domains": [ "healthcare.gov", "cuidadodesalud.gov", "cms.gov" ], - "Excluded": false + "excluded": false }, { - "Type": 34, - "Domains": [ + "type": 34, + "domains": [ "pepco.com", "pepcoholdings.com" ], - "Excluded": false + "excluded": false }, { - "Type": 35, - "Domains": [ + "type": 35, + "domains": [ "century21.com", "21online.com" ], - "Excluded": false + "excluded": false }, { - "Type": 36, - "Domains": [ + "type": 36, + "domains": [ "comcast.com", "comcast.net", "xfinity.com" ], - "Excluded": false + "excluded": false }, { - "Type": 37, - "Domains": [ + "type": 37, + "domains": [ "cricketwireless.com", "aiowireless.com" ], - "Excluded": false + "excluded": false }, { - "Type": 38, - "Domains": [ + "type": 38, + "domains": [ "mandtbank.com", "mtb.com" ], - "Excluded": false + "excluded": false }, { - "Type": 39, - "Domains": [ + "type": 39, + "domains": [ "dropbox.com", "getdropbox.com" ], - "Excluded": false + "excluded": false }, { - "Type": 40, - "Domains": [ + "type": 40, + "domains": [ "snapfish.com", "snapfish.ca" ], - "Excluded": false + "excluded": false }, { - "Type": 41, - "Domains": [ + "type": 41, + "domains": [ "alibaba.com", "aliexpress.com", "aliyun.com", "net.cn" ], - "Excluded": false + "excluded": false }, { - "Type": 42, - "Domains": [ + "type": 42, + "domains": [ "playstation.com", "sonyentertainmentnetwork.com" ], - "Excluded": false + "excluded": false }, { - "Type": 43, - "Domains": [ + "type": 43, + "domains": [ "mercadolivre.com", "mercadolivre.com.br", "mercadolibre.com", "mercadolibre.com.ar", "mercadolibre.com.mx" ], - "Excluded": false + "excluded": false }, { - "Type": 44, - "Domains": [ + "type": 44, + "domains": [ "zendesk.com", "zopim.com" ], - "Excluded": false + "excluded": false }, { - "Type": 45, - "Domains": [ + "type": 45, + "domains": [ "autodesk.com", "tinkercad.com" ], - "Excluded": false + "excluded": false }, { - "Type": 46, - "Domains": [ + "type": 46, + "domains": [ "railnation.ru", "railnation.de", "rail-nation.com", @@ -447,152 +447,152 @@ "trucknation.de", "traviangames.com" ], - "Excluded": false + "excluded": false }, { - "Type": 47, - "Domains": [ + "type": 47, + "domains": [ "wpcu.coop", "wpcuonline.com" ], - "Excluded": false + "excluded": false }, { - "Type": 48, - "Domains": [ + "type": 48, + "domains": [ "mathletics.com", "mathletics.com.au", "mathletics.co.uk" ], - "Excluded": false + "excluded": false }, { - "Type": 49, - "Domains": [ + "type": 49, + "domains": [ "discountbank.co.il", "telebank.co.il" ], - "Excluded": false + "excluded": false }, { - "Type": 50, - "Domains": [ + "type": 50, + "domains": [ "mi.com", "xiaomi.com" ], - "Excluded": false + "excluded": false }, { - "Type": 52, - "Domains": [ + "type": 52, + "domains": [ "postepay.it", "poste.it" ], - "Excluded": false + "excluded": false }, { - "Type": 51, - "Domains": [ + "type": 51, + "domains": [ "facebook.com", "messenger.com" ], - "Excluded": false + "excluded": false }, { - "Type": 53, - "Domains": [ + "type": 53, + "domains": [ "skysports.com", "skybet.com", "skyvegas.com" ], - "Excluded": false + "excluded": false }, { - "Type": 54, - "Domains": [ + "type": 54, + "domains": [ "disneymoviesanywhere.com", "go.com", "disney.com", "dadt.com", "disneyplus.com" ], - "Excluded": false + "excluded": false }, { - "Type": 55, - "Domains": [ + "type": 55, + "domains": [ "pokemon-gl.com", "pokemon.com" ], - "Excluded": false + "excluded": false }, { - "Type": 56, - "Domains": [ + "type": 56, + "domains": [ "myuv.com", "uvvu.com" ], - "Excluded": false + "excluded": false }, { - "Type": 58, - "Domains": [ + "type": 58, + "domains": [ "mdsol.com", "imedidata.com" ], - "Excluded": false + "excluded": false }, { - "Type": 57, - "Domains": [ + "type": 57, + "domains": [ "bank-yahav.co.il", "bankhapoalim.co.il" ], - "Excluded": false + "excluded": false }, { - "Type": 59, - "Domains": [ + "type": 59, + "domains": [ "sears.com", "shld.net" ], - "Excluded": false + "excluded": false }, { - "Type": 60, - "Domains": [ + "type": 60, + "domains": [ "xiami.com", "alipay.com" ], - "Excluded": false + "excluded": false }, { - "Type": 61, - "Domains": [ + "type": 61, + "domains": [ "belkin.com", "seedonk.com" ], - "Excluded": false + "excluded": false }, { - "Type": 62, - "Domains": [ + "type": 62, + "domains": [ "turbotax.com", "intuit.com" ], - "Excluded": false + "excluded": false }, { - "Type": 63, - "Domains": [ + "type": 63, + "domains": [ "shopify.com", "myshopify.com" ], - "Excluded": false + "excluded": false }, { - "Type": 64, - "Domains": [ + "type": 64, + "domains": [ "ebay.com", "ebay.at", "ebay.be", @@ -617,53 +617,53 @@ "ebay.ph", "ebay.pl" ], - "Excluded": false + "excluded": false }, { - "Type": 65, - "Domains": [ + "type": 65, + "domains": [ "techdata.com", "techdata.ch" ], - "Excluded": false + "excluded": false }, { - "Type": 66, - "Domains": [ + "type": 66, + "domains": [ "schwab.com", "schwabplan.com" ], - "Excluded": false + "excluded": false }, { - "Type": 68, - "Domains": [ + "type": 68, + "domains": [ "tesla.com", "teslamotors.com" ], - "Excluded": false + "excluded": false }, { - "Type": 69, - "Domains": [ + "type": 69, + "domains": [ "morganstanley.com", "morganstanleyclientserv.com", "stockplanconnect.com", "ms.com" ], - "Excluded": false + "excluded": false }, { - "Type": 70, - "Domains": [ + "type": 70, + "domains": [ "taxact.com", "taxactonline.com" ], - "Excluded": false + "excluded": false }, { - "Type": 71, - "Domains": [ + "type": 71, + "domains": [ "mediawiki.org", "wikibooks.org", "wikidata.org", @@ -676,11 +676,11 @@ "wikivoyage.org", "wiktionary.org" ], - "Excluded": false + "excluded": false }, { - "Type": 72, - "Domains": [ + "type": 72, + "domains": [ "airbnb.at", "airbnb.be", "airbnb.ca", @@ -735,11 +735,11 @@ "airbnb.ru", "airbnb.se" ], - "Excluded": false + "excluded": false }, { - "Type": 73, - "Domains": [ + "type": 73, + "domains": [ "eventbrite.at", "eventbrite.be", "eventbrite.ca", @@ -767,11 +767,11 @@ "eventbrite.se", "eventbrite.sg" ], - "Excluded": false + "excluded": false }, { - "Type": 74, - "Domains": [ + "type": 74, + "domains": [ "stackexchange.com", "superuser.com", "stackoverflow.com", @@ -780,19 +780,19 @@ "askubuntu.com", "stackapps.com" ], - "Excluded": false + "excluded": false }, { - "Type": 75, - "Domains": [ + "type": 75, + "domains": [ "docusign.com", "docusign.net" ], - "Excluded": false + "excluded": false }, { - "Type": 76, - "Domains": [ + "type": 76, + "domains": [ "envato.com", "themeforest.net", "codecanyon.net", @@ -802,28 +802,28 @@ "photodune.net", "3docean.net" ], - "Excluded": false + "excluded": false }, { - "Type": 77, - "Domains": [ + "type": 77, + "domains": [ "x10hosting.com", "x10premium.com" ], - "Excluded": false + "excluded": false }, { - "Type": 78, - "Domains": [ + "type": 78, + "domains": [ "dnsomatic.com", "opendns.com", "umbrella.com" ], - "Excluded": false + "excluded": false }, { - "Type": 79, - "Domains": [ + "type": 79, + "domains": [ "cagreatamerica.com", "canadaswonderland.com", "carowinds.com", @@ -838,36 +838,36 @@ "visitkingsisland.com", "worldsoffun.com" ], - "Excluded": false + "excluded": false }, { - "Type": 80, - "Domains": [ + "type": 80, + "domains": [ "ubnt.com", "ui.com" ], - "Excluded": false + "excluded": false }, { - "Type": 81, - "Domains": [ + "type": 81, + "domains": [ "discordapp.com", "discord.com" ], - "Excluded": false + "excluded": false }, { - "Type": 82, - "Domains": [ + "type": 82, + "domains": [ "netcup.de", "netcup.eu", "customercontrolpanel.de" ], - "Excluded": false + "excluded": false }, { - "Type": 83, - "Domains": [ + "type": 83, + "domains": [ "yandex.com", "ya.ru", "yandex.az", @@ -891,44 +891,44 @@ "yandex.ua", "yandex.uz" ], - "Excluded": false + "excluded": false }, { - "Type": 84, - "Domains": [ + "type": 84, + "domains": [ "sonyentertainmentnetwork.com", "sony.com" ], - "Excluded": false + "excluded": false }, { - "Type": 85, - "Domains": [ + "type": 85, + "domains": [ "proton.me", "protonmail.com", "protonvpn.com" ], - "Excluded": false + "excluded": false }, { - "Type": 86, - "Domains": [ + "type": 86, + "domains": [ "ubisoft.com", "ubi.com" ], - "Excluded": false + "excluded": false }, { - "Type": 87, - "Domains": [ + "type": 87, + "domains": [ "transferwise.com", "wise.com" ], - "Excluded": false + "excluded": false }, { - "Type": 88, - "Domains": [ + "type": 88, + "domains": [ "takeaway.com", "just-eat.dk", "just-eat.no", @@ -939,11 +939,11 @@ "thuisbezorgd.nl", "pyszne.pl" ], - "Excluded": false + "excluded": false }, { - "Type": 89, - "Domains": [ + "type": 89, + "domains": [ "atlassian.com", "bitbucket.org", "trello.com", @@ -951,11 +951,11 @@ "atlassian.net", "jira.com" ], - "Excluded": false + "excluded": false }, { - "Type": 90, - "Domains": [ + "type": 90, + "domains": [ "pinterest.com", "pinterest.com.au", "pinterest.cl", @@ -970,6 +970,6 @@ "pinterest.pt", "pinterest.se" ], - "Excluded": false + "excluded": false } -] -\ No newline at end of file +] diff --git a/src/static/scripts/bootstrap.bundle.js b/src/static/scripts/bootstrap.bundle.js @@ -1,6 +1,6 @@ /*! - * Bootstrap v5.3.1 (https://getbootstrap.com/) - * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ (function (global, factory) { @@ -210,7 +210,6 @@ const reflow = element => { element.offsetHeight; // eslint-disable-line no-unused-expressions }; - const getjQuery = () => { if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { return window.jQuery; @@ -648,7 +647,7 @@ * Constants */ - const VERSION = '5.3.1'; + const VERSION = '5.3.3'; /** * Class definition @@ -731,7 +730,7 @@ } selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null; } - return parseSelector(selector); + return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null; }; const SelectorEngine = { find(selector, element = document.documentElement) { @@ -3916,7 +3915,6 @@ // if false, we use the backdrop helper without adding any element to the dom rootElement: 'body' // give the choice to place backdrop under different elements }; - const DefaultType$8 = { className: 'string', clickCallback: '(function|null)', @@ -4041,7 +4039,6 @@ autofocus: true, trapElement: null // The element to trap focus inside of }; - const DefaultType$7 = { autofocus: 'boolean', trapElement: 'element' @@ -4768,7 +4765,10 @@ br: [], col: [], code: [], + dd: [], div: [], + dl: [], + dt: [], em: [], hr: [], h1: [], @@ -5866,7 +5866,7 @@ const CLASS_DROPDOWN = 'dropdown'; const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'; - const NOT_SELECTOR_DROPDOWN_TOGGLE = ':not(.dropdown-toggle)'; + const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`; const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'; const SELECTOR_OUTER = '.nav-item, .list-group-item'; const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`; @@ -6311,3 +6311,4 @@ return index_umd; })); +//# sourceMappingURL=bootstrap.bundle.js.map diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css @@ -1,7 +1,7 @@ @charset "UTF-8"; /*! - * Bootstrap v5.3.1 (https://getbootstrap.com/) - * Copyright 2011-2023 The Bootstrap Authors + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ :root, @@ -99,6 +99,7 @@ --bs-link-hover-color: #0a58ca; --bs-link-hover-color-rgb: 10, 88, 202; --bs-code-color: #d63384; + --bs-highlight-color: #212529; --bs-highlight-bg: #fff3cd; --bs-border-width: 1px; --bs-border-style: solid; @@ -170,6 +171,8 @@ --bs-link-color-rgb: 110, 168, 254; --bs-link-hover-color-rgb: 139, 185, 254; --bs-code-color: #e685b5; + --bs-highlight-color: #dee2e6; + --bs-highlight-bg: #664d03; --bs-border-color: #495057; --bs-border-color-translucent: rgba(255, 255, 255, 0.15); --bs-form-valid-color: #75b798; @@ -325,6 +328,7 @@ small, .small { mark, .mark { padding: 0.1875em; + color: var(--bs-highlight-color); background-color: var(--bs-highlight-bg); } @@ -819,7 +823,7 @@ progress { .row-cols-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } .row-cols-4 > * { @@ -834,7 +838,7 @@ progress { .row-cols-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } .col-auto { @@ -1024,7 +1028,7 @@ progress { } .row-cols-sm-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } .row-cols-sm-4 > * { flex: 0 0 auto; @@ -1036,7 +1040,7 @@ progress { } .row-cols-sm-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } .col-sm-auto { flex: 0 0 auto; @@ -1193,7 +1197,7 @@ progress { } .row-cols-md-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } .row-cols-md-4 > * { flex: 0 0 auto; @@ -1205,7 +1209,7 @@ progress { } .row-cols-md-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } .col-md-auto { flex: 0 0 auto; @@ -1362,7 +1366,7 @@ progress { } .row-cols-lg-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } .row-cols-lg-4 > * { flex: 0 0 auto; @@ -1374,7 +1378,7 @@ progress { } .row-cols-lg-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } .col-lg-auto { flex: 0 0 auto; @@ -1531,7 +1535,7 @@ progress { } .row-cols-xl-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } .row-cols-xl-4 > * { flex: 0 0 auto; @@ -1543,7 +1547,7 @@ progress { } .row-cols-xl-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } .col-xl-auto { flex: 0 0 auto; @@ -1700,7 +1704,7 @@ progress { } .row-cols-xxl-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } .row-cols-xxl-4 > * { flex: 0 0 auto; @@ -1712,7 +1716,7 @@ progress { } .row-cols-xxl-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } .col-xxl-auto { flex: 0 0 auto; @@ -1856,16 +1860,16 @@ progress { --bs-table-bg-type: initial; --bs-table-color-state: initial; --bs-table-bg-state: initial; - --bs-table-color: var(--bs-body-color); + --bs-table-color: var(--bs-emphasis-color); --bs-table-bg: var(--bs-body-bg); --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); + --bs-table-striped-color: var(--bs-emphasis-color); + --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05); + --bs-table-active-color: var(--bs-emphasis-color); + --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1); + --bs-table-hover-color: var(--bs-emphasis-color); + --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075); width: 100%; margin-bottom: 1rem; vertical-align: top; @@ -1934,7 +1938,7 @@ progress { .table-primary { --bs-table-color: #000; --bs-table-bg: #cfe2ff; - --bs-table-border-color: #bacbe6; + --bs-table-border-color: #a6b5cc; --bs-table-striped-bg: #c5d7f2; --bs-table-striped-color: #000; --bs-table-active-bg: #bacbe6; @@ -1948,7 +1952,7 @@ progress { .table-secondary { --bs-table-color: #000; --bs-table-bg: #e2e3e5; - --bs-table-border-color: #cbccce; + --bs-table-border-color: #b5b6b7; --bs-table-striped-bg: #d7d8da; --bs-table-striped-color: #000; --bs-table-active-bg: #cbccce; @@ -1962,7 +1966,7 @@ progress { .table-success { --bs-table-color: #000; --bs-table-bg: #d1e7dd; - --bs-table-border-color: #bcd0c7; + --bs-table-border-color: #a7b9b1; --bs-table-striped-bg: #c7dbd2; --bs-table-striped-color: #000; --bs-table-active-bg: #bcd0c7; @@ -1976,7 +1980,7 @@ progress { .table-info { --bs-table-color: #000; --bs-table-bg: #cff4fc; - --bs-table-border-color: #badce3; + --bs-table-border-color: #a6c3ca; --bs-table-striped-bg: #c5e8ef; --bs-table-striped-color: #000; --bs-table-active-bg: #badce3; @@ -1990,7 +1994,7 @@ progress { .table-warning { --bs-table-color: #000; --bs-table-bg: #fff3cd; - --bs-table-border-color: #e6dbb9; + --bs-table-border-color: #ccc2a4; --bs-table-striped-bg: #f2e7c3; --bs-table-striped-color: #000; --bs-table-active-bg: #e6dbb9; @@ -2004,7 +2008,7 @@ progress { .table-danger { --bs-table-color: #000; --bs-table-bg: #f8d7da; - --bs-table-border-color: #dfc2c4; + --bs-table-border-color: #c6acae; --bs-table-striped-bg: #eccccf; --bs-table-striped-color: #000; --bs-table-active-bg: #dfc2c4; @@ -2018,7 +2022,7 @@ progress { .table-light { --bs-table-color: #000; --bs-table-bg: #f8f9fa; - --bs-table-border-color: #dfe0e1; + --bs-table-border-color: #c6c7c8; --bs-table-striped-bg: #ecedee; --bs-table-striped-color: #000; --bs-table-active-bg: #dfe0e1; @@ -2032,7 +2036,7 @@ progress { .table-dark { --bs-table-color: #fff; --bs-table-bg: #212529; - --bs-table-border-color: #373b3e; + --bs-table-border-color: #4d5154; --bs-table-striped-bg: #2c3034; --bs-table-striped-color: #fff; --bs-table-active-bg: #373b3e; @@ -2388,6 +2392,7 @@ textarea.form-control-lg { .form-check-input { --bs-form-check-bg: var(--bs-body-bg); + flex-shrink: 0; width: 1em; height: 1em; margin-top: 0.25em; @@ -2544,7 +2549,7 @@ textarea.form-control-lg { height: 0.5rem; color: transparent; cursor: pointer; - background-color: var(--bs-tertiary-bg); + background-color: var(--bs-secondary-bg); border-color: transparent; border-radius: 1rem; } @@ -2573,7 +2578,7 @@ textarea.form-control-lg { height: 0.5rem; color: transparent; cursor: pointer; - background-color: var(--bs-tertiary-bg); + background-color: var(--bs-secondary-bg); border-color: transparent; border-radius: 1rem; } @@ -3037,6 +3042,9 @@ textarea.form-control-lg { .btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible { box-shadow: var(--bs-btn-focus-box-shadow); } +.btn-check:checked:focus-visible + .btn { + box-shadow: var(--bs-btn-focus-box-shadow); +} .btn:disabled, .btn.disabled, fieldset:disabled .btn { color: var(--bs-btn-disabled-color); pointer-events: none; @@ -3431,7 +3439,7 @@ textarea.form-control-lg { --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width)); --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-box-shadow: var(--bs-box-shadow); --bs-dropdown-link-color: var(--bs-body-color); --bs-dropdown-link-hover-color: var(--bs-body-color); --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg); @@ -4568,12 +4576,11 @@ textarea.form-control-lg { --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='%23212529'%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: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%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='%23052c65'%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-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e"); --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; @@ -4631,7 +4638,6 @@ textarea.form-control-lg { } .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); } @@ -4649,7 +4655,7 @@ textarea.form-control-lg { 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 { +.accordion-item:first-of-type > .accordion-header .accordion-button { border-top-left-radius: var(--bs-accordion-inner-border-radius); border-top-right-radius: var(--bs-accordion-inner-border-radius); } @@ -4660,11 +4666,11 @@ textarea.form-control-lg { 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 { +.accordion-item:last-of-type > .accordion-header .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 { +.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); } @@ -4673,21 +4679,21 @@ textarea.form-control-lg { padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); } -.accordion-flush .accordion-collapse { - border-width: 0; -} -.accordion-flush .accordion-item { +.accordion-flush > .accordion-item { border-right: 0; border-left: 0; border-radius: 0; } -.accordion-flush .accordion-item:first-child { +.accordion-flush > .accordion-item:first-child { border-top: 0; } -.accordion-flush .accordion-item:last-child { +.accordion-flush > .accordion-item:last-child { border-bottom: 0; } -.accordion-flush .accordion-item .accordion-button, .accordion-flush .accordion-item .accordion-button.collapsed { +.accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed { + border-radius: 0; +} +.accordion-flush > .accordion-item > .accordion-collapse { border-radius: 0; } @@ -5473,7 +5479,7 @@ textarea.form-control-lg { --bs-modal-border-color: var(--bs-border-color-translucent); --bs-modal-border-width: var(--bs-border-width); --bs-modal-border-radius: var(--bs-border-radius-lg); - --bs-modal-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-modal-box-shadow: var(--bs-box-shadow-sm); --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width))); --bs-modal-header-padding-x: 1rem; --bs-modal-header-padding-y: 1rem; @@ -5573,7 +5579,6 @@ textarea.form-control-lg { 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); @@ -5614,7 +5619,7 @@ textarea.form-control-lg { @media (min-width: 576px) { .modal { --bs-modal-margin: 1.75rem; - --bs-modal-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-modal-box-shadow: var(--bs-box-shadow); } .modal-dialog { max-width: var(--bs-modal-width); @@ -5866,7 +5871,7 @@ textarea.form-control-lg { --bs-popover-border-color: var(--bs-border-color-translucent); --bs-popover-border-radius: var(--bs-border-radius-lg); --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width)); - --bs-popover-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-popover-box-shadow: var(--bs-box-shadow); --bs-popover-header-padding-x: 1rem; --bs-popover-header-padding-y: 0.5rem; --bs-popover-header-font-size: 1rem; @@ -6139,20 +6144,12 @@ textarea.form-control-lg { background-size: 100% 100%; } -/* rtl:options: { - "autoRename": true, - "stringMap":[ { - "name" : "prev-next", - "search" : "prev", - "replace" : "next" - } ] -} */ .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"); + 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") /*rtl: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-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"); + 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") /*rtl: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-indicators { @@ -6301,7 +6298,7 @@ textarea.form-control-lg { --bs-offcanvas-bg: var(--bs-body-bg); --bs-offcanvas-border-width: var(--bs-border-width); --bs-offcanvas-border-color: var(--bs-border-color-translucent); - --bs-offcanvas-box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm); --bs-offcanvas-transition: transform 0.3s ease-in-out; --bs-offcanvas-title-line-height: 1.5; } @@ -6772,14 +6769,11 @@ textarea.form-control-lg { .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) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5); - margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y)); - margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x)); - margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin: calc(-0.5 * var(--bs-offcanvas-padding-y)) calc(-0.5 * var(--bs-offcanvas-padding-x)) calc(-0.5 * var(--bs-offcanvas-padding-y)) auto; } .offcanvas-title { @@ -7380,15 +7374,15 @@ textarea.form-control-lg { } .shadow { - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + box-shadow: var(--bs-box-shadow) !important; } .shadow-sm { - box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + box-shadow: var(--bs-box-shadow-sm) !important; } .shadow-lg { - box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; + box-shadow: var(--bs-box-shadow-lg) !important; } .shadow-none { @@ -12059,3 +12053,5 @@ textarea.form-control-lg { display: none !important; } } + +/*# sourceMappingURL=bootstrap.css.map */ +\ No newline at end of file diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.13.6 + * https://datatables.net/download/#bs5/dt-2.0.8 * * Included libraries: - * DataTables 1.13.6 + * DataTables 2.0.8 */ @charset "UTF-8"; @@ -30,76 +30,124 @@ table.dataTable td.dt-control { } table.dataTable td.dt-control:before { display: inline-block; - color: rgba(0, 0, 0, 0.5); - content: "►"; + box-sizing: border-box; + content: ""; + border-top: 5px solid transparent; + border-left: 10px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid transparent; + border-right: 0px solid transparent; } table.dataTable tr.dt-hasChild td.dt-control:before { - content: "▼"; + border-top: 10px solid rgba(0, 0, 0, 0.5); + border-left: 5px solid transparent; + border-bottom: 0px solid transparent; + border-right: 5px solid transparent; } -html.dark table.dataTable td.dt-control:before { - color: rgba(255, 255, 255, 0.5); +html.dark table.dataTable td.dt-control:before, +:root[data-bs-theme=dark] table.dataTable td.dt-control:before { + border-left-color: rgba(255, 255, 255, 0.5); } -html.dark table.dataTable tr.dt-hasChild td.dt-control:before { - color: rgba(255, 255, 255, 0.5); +html.dark table.dataTable tr.dt-hasChild td.dt-control:before, +:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before { + border-top-color: rgba(255, 255, 255, 0.5); + border-left-color: transparent; } -table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled, -table.dataTable thead > tr > td.sorting, -table.dataTable thead > tr > td.sorting_asc, -table.dataTable thead > tr > td.sorting_desc, -table.dataTable thead > tr > td.sorting_asc_disabled, -table.dataTable thead > tr > td.sorting_desc_disabled { - cursor: pointer; - position: relative; - padding-right: 26px; -} -table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:after, -table.dataTable thead > tr > td.sorting:before, -table.dataTable thead > tr > td.sorting:after, -table.dataTable thead > tr > td.sorting_asc:before, -table.dataTable thead > tr > td.sorting_asc:after, -table.dataTable thead > tr > td.sorting_desc:before, -table.dataTable thead > tr > td.sorting_desc:after, -table.dataTable thead > tr > td.sorting_asc_disabled:before, -table.dataTable thead > tr > td.sorting_asc_disabled:after, -table.dataTable thead > tr > td.sorting_desc_disabled:before, -table.dataTable thead > tr > td.sorting_desc_disabled:after { +div.dt-scroll-body thead tr, +div.dt-scroll-body tfoot tr { + height: 0; +} +div.dt-scroll-body thead tr th, div.dt-scroll-body thead tr td, +div.dt-scroll-body tfoot tr th, +div.dt-scroll-body tfoot tr td { + height: 0 !important; + padding-top: 0px !important; + padding-bottom: 0px !important; + border-top-width: 0px !important; + border-bottom-width: 0px !important; +} +div.dt-scroll-body thead tr th div.dt-scroll-sizing, div.dt-scroll-body thead tr td div.dt-scroll-sizing, +div.dt-scroll-body tfoot tr th div.dt-scroll-sizing, +div.dt-scroll-body tfoot tr td div.dt-scroll-sizing { + height: 0 !important; + overflow: hidden !important; +} + +table.dataTable thead > tr > th:active, +table.dataTable thead > tr > td:active { + outline: none; +} +table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before { position: absolute; display: block; - opacity: 0.125; - right: 10px; - line-height: 9px; - font-size: 0.8em; -} -table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before, -table.dataTable thead > tr > td.sorting:before, -table.dataTable thead > tr > td.sorting_asc:before, -table.dataTable thead > tr > td.sorting_desc:before, -table.dataTable thead > tr > td.sorting_asc_disabled:before, -table.dataTable thead > tr > td.sorting_desc_disabled:before { bottom: 50%; content: "▲"; content: "▲"/""; } -table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after, -table.dataTable thead > tr > td.sorting:after, -table.dataTable thead > tr > td.sorting_asc:after, -table.dataTable thead > tr > td.sorting_desc:after, -table.dataTable thead > tr > td.sorting_asc_disabled:after, -table.dataTable thead > tr > td.sorting_desc_disabled:after { +table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { + position: absolute; + display: block; top: 50%; content: "▼"; content: "▼"/""; } -table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after, -table.dataTable thead > tr > td.sorting_asc:before, -table.dataTable thead > tr > td.sorting_desc:after { +table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, table.dataTable thead > tr > th.dt-ordering-asc, table.dataTable thead > tr > th.dt-ordering-desc, +table.dataTable thead > tr > td.dt-orderable-asc, +table.dataTable thead > tr > td.dt-orderable-desc, +table.dataTable thead > tr > td.dt-ordering-asc, +table.dataTable thead > tr > td.dt-ordering-desc { + position: relative; + padding-right: 30px; +} +table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order, +table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order, +table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order, +table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order, +table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order { + position: absolute; + right: 12px; + top: 0; + bottom: 0; + width: 12px; +} +table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:after, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order:after, +table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:before, +table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { + left: 0; + opacity: 0.125; + line-height: 9px; + font-size: 0.8em; +} +table.dataTable thead > tr > th.dt-orderable-asc, table.dataTable thead > tr > th.dt-orderable-desc, +table.dataTable thead > tr > td.dt-orderable-asc, +table.dataTable thead > tr > td.dt-orderable-desc { + cursor: pointer; +} +table.dataTable thead > tr > th.dt-orderable-asc:hover, table.dataTable thead > tr > th.dt-orderable-desc:hover, +table.dataTable thead > tr > td.dt-orderable-asc:hover, +table.dataTable thead > tr > td.dt-orderable-desc:hover { + outline: 2px solid rgba(0, 0, 0, 0.05); + outline-offset: -2px; +} +table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order:before, table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order:after, +table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order:before, +table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order:after { opacity: 0.6; } -table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > th.sorting_asc_disabled:before, -table.dataTable thead > tr > td.sorting_desc_disabled:after, -table.dataTable thead > tr > td.sorting_asc_disabled:before { +table.dataTable thead > tr > th.sorting_desc_disabled span.dt-column-order:after, table.dataTable thead > tr > th.sorting_asc_disabled span.dt-column-order:before, +table.dataTable thead > tr > td.sorting_desc_disabled span.dt-column-order:after, +table.dataTable thead > tr > td.sorting_asc_disabled span.dt-column-order:before { display: none; } table.dataTable thead > tr > th:active, @@ -107,29 +155,39 @@ table.dataTable thead > tr > td:active { outline: none; } -div.dataTables_scrollBody > table.dataTable > thead > tr > th:before, div.dataTables_scrollBody > table.dataTable > thead > tr > th:after, -div.dataTables_scrollBody > table.dataTable > thead > tr > td:before, -div.dataTables_scrollBody > table.dataTable > thead > tr > td:after { - display: none; +div.dt-scroll-body > table.dataTable > thead > tr > th, +div.dt-scroll-body > table.dataTable > thead > tr > td { + overflow: hidden; +} + +:root.dark table.dataTable thead > tr > th.dt-orderable-asc:hover, :root.dark table.dataTable thead > tr > th.dt-orderable-desc:hover, +:root.dark table.dataTable thead > tr > td.dt-orderable-asc:hover, +:root.dark table.dataTable thead > tr > td.dt-orderable-desc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-asc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > th.dt-orderable-desc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-asc:hover, +:root[data-bs-theme=dark] table.dataTable thead > tr > td.dt-orderable-desc:hover { + outline: 2px solid rgba(255, 255, 255, 0.05); } -div.dataTables_processing { +div.dt-processing { position: absolute; top: 50%; left: 50%; width: 200px; margin-left: -100px; - margin-top: -26px; + margin-top: -22px; text-align: center; padding: 2px; + z-index: 10; } -div.dataTables_processing > div:last-child { +div.dt-processing > div:last-child { position: relative; width: 80px; height: 15px; margin: 1em auto; } -div.dataTables_processing > div:last-child > div { +div.dt-processing > div:last-child > div { position: absolute; top: 0; width: 13px; @@ -139,19 +197,19 @@ div.dataTables_processing > div:last-child > div { background: rgb(var(--dt-row-selected)); animation-timing-function: cubic-bezier(0, 1, 1, 0); } -div.dataTables_processing > div:last-child > div:nth-child(1) { +div.dt-processing > div:last-child > div:nth-child(1) { left: 8px; animation: datatables-loader-1 0.6s infinite; } -div.dataTables_processing > div:last-child > div:nth-child(2) { +div.dt-processing > div:last-child > div:nth-child(2) { left: 8px; animation: datatables-loader-2 0.6s infinite; } -div.dataTables_processing > div:last-child > div:nth-child(3) { +div.dt-processing > div:last-child > div:nth-child(3) { left: 32px; animation: datatables-loader-2 0.6s infinite; } -div.dataTables_processing > div:last-child > div:nth-child(4) { +div.dt-processing > div:last-child > div:nth-child(4) { left: 56px; animation: datatables-loader-3 0.6s infinite; } @@ -183,13 +241,16 @@ div.dataTables_processing > div:last-child > div:nth-child(4) { table.dataTable.nowrap th, table.dataTable.nowrap td { white-space: nowrap; } +table.dataTable th, +table.dataTable td { + box-sizing: border-box; +} table.dataTable th.dt-left, table.dataTable td.dt-left { text-align: left; } table.dataTable th.dt-center, -table.dataTable td.dt-center, -table.dataTable td.dataTables_empty { +table.dataTable td.dt-center { text-align: center; } table.dataTable th.dt-right, @@ -204,6 +265,16 @@ table.dataTable th.dt-nowrap, table.dataTable td.dt-nowrap { white-space: nowrap; } +table.dataTable th.dt-empty, +table.dataTable td.dt-empty { + text-align: center; + vertical-align: top; +} +table.dataTable th.dt-type-numeric, table.dataTable th.dt-type-date, +table.dataTable td.dt-type-numeric, +table.dataTable td.dt-type-date { + text-align: right; +} table.dataTable thead th, table.dataTable thead td, table.dataTable tfoot th, @@ -266,179 +337,158 @@ table.dataTable tbody td.dt-body-nowrap { * ©2020 SpryMedia Ltd, all rights reserved. * License: MIT datatables.net/license/mit */ -table.dataTable { +table.table.dataTable { clear: both; - margin-top: 6px !important; - margin-bottom: 6px !important; - max-width: none !important; - border-collapse: separate !important; + margin-bottom: 0; + max-width: none; border-spacing: 0; } -table.dataTable td, -table.dataTable th { - -webkit-box-sizing: content-box; - box-sizing: content-box; -} -table.dataTable td.dataTables_empty, -table.dataTable th.dataTables_empty { - text-align: center; -} -table.dataTable.nowrap th, -table.dataTable.nowrap td { - white-space: nowrap; -} -table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { +table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { box-shadow: none; } -table.dataTable > tbody > tr { +table.table.dataTable > :not(caption) > * > * { + background-color: var(--bs-table-bg); +} +table.table.dataTable > tbody > tr { background-color: transparent; } -table.dataTable > tbody > tr.selected > * { +table.table.dataTable > tbody > tr.selected > * { box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected)); color: rgb(255, 255, 255); color: rgb(var(--dt-row-selected-text)); } -table.dataTable > tbody > tr.selected a { +table.table.dataTable > tbody > tr.selected a { color: rgb(9, 10, 11); color: rgb(var(--dt-row-selected-link)); } -table.dataTable.table-striped > tbody > tr.odd > * { +table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05); } -table.dataTable.table-striped > tbody > tr.odd.selected > * { +table.table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95); box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95); } -table.dataTable.table-hover > tbody > tr:hover > * { +table.table.dataTable.table-hover > tbody > tr:hover > * { box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075); } -table.dataTable.table-hover > tbody > tr.selected:hover > * { +table.table.dataTable.table-hover > tbody > tr.selected:hover > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975); box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); } -div.dataTables_wrapper div.dataTables_length label { +div.dt-container div.dt-length label { font-weight: normal; text-align: left; white-space: nowrap; } -div.dataTables_wrapper div.dataTables_length select { +div.dt-container div.dt-length select { width: auto; display: inline-block; + margin-right: 0.5em; } -div.dataTables_wrapper div.dataTables_filter { +div.dt-container div.dt-search { text-align: right; } -div.dataTables_wrapper div.dataTables_filter label { +div.dt-container div.dt-search label { font-weight: normal; white-space: nowrap; text-align: left; } -div.dataTables_wrapper div.dataTables_filter input { +div.dt-container div.dt-search input { margin-left: 0.5em; display: inline-block; width: auto; } -div.dataTables_wrapper div.dataTables_info { +div.dt-container div.dt-info { padding-top: 0.85em; } -div.dataTables_wrapper div.dataTables_paginate { +div.dt-container div.dt-paging { margin: 0; - white-space: nowrap; - text-align: right; } -div.dataTables_wrapper div.dataTables_paginate ul.pagination { +div.dt-container div.dt-paging ul.pagination { margin: 2px 0; - white-space: nowrap; - justify-content: flex-end; + flex-wrap: wrap; } -div.dataTables_wrapper div.dt-row { +div.dt-container div.dt-row { position: relative; } -div.dataTables_scrollHead table.dataTable { +div.dt-scroll-head table.dataTable { margin-bottom: 0 !important; } -div.dataTables_scrollBody > table { +div.dt-scroll-body { + border-bottom-color: var(--bs-border-color); + border-bottom-width: var(--bs-border-width); + border-bottom-style: solid; +} +div.dt-scroll-body > table { border-top: none; margin-top: 0 !important; margin-bottom: 0 !important; } -div.dataTables_scrollBody > table > thead .sorting:before, -div.dataTables_scrollBody > table > thead .sorting_asc:before, -div.dataTables_scrollBody > table > thead .sorting_desc:before, -div.dataTables_scrollBody > table > thead .sorting:after, -div.dataTables_scrollBody > table > thead .sorting_asc:after, -div.dataTables_scrollBody > table > thead .sorting_desc:after { - display: none; +div.dt-scroll-body > table > tbody > tr:first-child { + border-top-width: 0; } -div.dataTables_scrollBody > table > tbody tr:first-child th, -div.dataTables_scrollBody > table > tbody tr:first-child td { - border-top: none; +div.dt-scroll-body > table > thead > tr { + border-width: 0 !important; +} +div.dt-scroll-body > table > tbody > tr:last-child > * { + border-bottom: none; } -div.dataTables_scrollFoot > .dataTables_scrollFootInner { +div.dt-scroll-foot > .dt-scroll-footInner { box-sizing: content-box; } -div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { +div.dt-scroll-foot > .dt-scroll-footInner > table { margin-top: 0 !important; border-top: none; } +div.dt-scroll-foot > .dt-scroll-footInner > table > tfoot > tr:first-child { + border-top-width: 0 !important; +} @media screen and (max-width: 767px) { - div.dataTables_wrapper div.dataTables_length, - div.dataTables_wrapper div.dataTables_filter, - div.dataTables_wrapper div.dataTables_info, - div.dataTables_wrapper div.dataTables_paginate { + div.dt-container div.dt-length, + div.dt-container div.dt-search, + div.dt-container div.dt-info, + div.dt-container div.dt-paging { text-align: center; } - div.dataTables_wrapper div.dataTables_paginate ul.pagination { + div.dt-container .row { + --bs-gutter-y: 0.5rem; + } + div.dt-container div.dt-paging ul.pagination { justify-content: center !important; } } -table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) { +table.dataTable.table-sm > thead > tr th.dt-orderable-asc, table.dataTable.table-sm > thead > tr th.dt-orderable-desc, table.dataTable.table-sm > thead > tr th.dt-ordering-asc, table.dataTable.table-sm > thead > tr th.dt-ordering-desc, +table.dataTable.table-sm > thead > tr td.dt-orderable-asc, +table.dataTable.table-sm > thead > tr td.dt-orderable-desc, +table.dataTable.table-sm > thead > tr td.dt-ordering-asc, +table.dataTable.table-sm > thead > tr td.dt-ordering-desc { padding-right: 20px; } - -table.table-bordered.dataTable { - border-right-width: 0; -} -table.table-bordered.dataTable thead tr:first-child th, -table.table-bordered.dataTable thead tr:first-child td { - border-top-width: 1px; -} -table.table-bordered.dataTable th, -table.table-bordered.dataTable td { - border-left-width: 0; -} -table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child, -table.table-bordered.dataTable td:first-child, -table.table-bordered.dataTable td:first-child { - border-left-width: 1px; -} -table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, -table.table-bordered.dataTable td:last-child, -table.table-bordered.dataTable td:last-child { - border-right-width: 1px; -} -table.table-bordered.dataTable th, -table.table-bordered.dataTable td { - border-bottom-width: 1px; +table.dataTable.table-sm > thead > tr th.dt-orderable-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-orderable-desc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-asc span.dt-column-order, table.dataTable.table-sm > thead > tr th.dt-ordering-desc span.dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-orderable-asc span.dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-orderable-desc span.dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-ordering-asc span.dt-column-order, +table.dataTable.table-sm > thead > tr td.dt-ordering-desc span.dt-column-order { + right: 5px; } -div.dataTables_scrollHead table.table-bordered { +div.dt-scroll-head table.table-bordered { border-bottom-width: 0; } -div.table-responsive > div.dataTables_wrapper > div.row { +div.table-responsive > div.dt-container > div.row { margin: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child { +div.table-responsive > div.dt-container > div.row > div[class^=col-]:first-child { padding-left: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child { +div.table-responsive > div.dt-container > div.row > div[class^=col-]:last-child { padding-right: 0; } diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js @@ -4,37 +4,34 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.13.6 + * https://datatables.net/download/#bs5/dt-2.0.8 * * Included libraries: - * DataTables 1.13.6 + * DataTables 2.0.8 */ -/*! DataTables 1.13.6 - * ©2008-2023 SpryMedia Ltd - datatables.net/license +/*! DataTables 2.0.8 + * © SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 1.13.6 + * @version 2.0.8 * @author SpryMedia Ltd * @contact www.datatables.net * @copyright SpryMedia Ltd. * * This source file is free software, available under the following license: - * MIT license - http://datatables.net/license + * MIT license - https://datatables.net/license * * This source file is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. * - * For details please refer to: http://www.datatables.net + * For details please refer to: https://www.datatables.net */ -/*jslint evil: true, undef: true, browser: true */ -/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/ - (function( factory ) { "use strict"; @@ -66,15 +63,14 @@ }; } else { - return factory( jq, window, window.document ); + module.exports = factory( jq, window, window.document ); } } else { // Browser window.DataTable = factory( jQuery, window, document ); } -} -(function( $, window, document, undefined ) { +}(function( $, window, document ) { "use strict"; @@ -95,968 +91,219 @@ options = selector; } - /** - * Perform a jQuery selector action on the table's TR elements (from the tbody) and - * return the resulting jQuery object. - * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on - * @param {object} [oOpts] Optional parameters for modifying the rows to be included - * @param {string} [oOpts.filter=none] Select TR elements that meet the current filter - * criterion ("applied") or all TR elements (i.e. no filter). - * @param {string} [oOpts.order=current] Order of the TR elements in the processed array. - * Can be either 'current', whereby the current sorting of the table is used, or - * 'original' whereby the original order the data was read into the table is used. - * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page - * ("current") or not ("all"). If 'current' is given, then order is assumed to be - * 'current' and filter is 'applied', regardless of what they might be given as. - * @returns {object} jQuery object, filtered by the given selector. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Highlight every second row - * oTable.$('tr:odd').css('backgroundColor', 'blue'); - * } ); - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Filter to rows with 'Webkit' in them, add a background colour and then - * // remove the filter, thus highlighting the 'Webkit' rows only. - * oTable.fnFilter('Webkit'); - * oTable.$('tr', {"search": "applied"}).css('backgroundColor', 'blue'); - * oTable.fnFilter(''); - * } ); - */ - this.$ = function ( sSelector, oOpts ) - { - return this.api(true).$( sSelector, oOpts ); - }; - - - /** - * Almost identical to $ in operation, but in this case returns the data for the matched - * rows - as such, the jQuery selector used should match TR row nodes or TD/TH cell nodes - * rather than any descendants, so the data can be obtained for the row/cell. If matching - * rows are found, the data returned is the original data array/object that was used to - * create the row (or a generated array if from a DOM source). - * - * This method is often useful in-combination with $ where both functions are given the - * same parameters and the array indexes will match identically. - * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on - * @param {object} [oOpts] Optional parameters for modifying the rows to be included - * @param {string} [oOpts.filter=none] Select elements that meet the current filter - * criterion ("applied") or all elements (i.e. no filter). - * @param {string} [oOpts.order=current] Order of the data in the processed array. - * Can be either 'current', whereby the current sorting of the table is used, or - * 'original' whereby the original order the data was read into the table is used. - * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page - * ("current") or not ("all"). If 'current' is given, then order is assumed to be - * 'current' and filter is 'applied', regardless of what they might be given as. - * @returns {array} Data for the matched elements. If any elements, as a result of the - * selector, were not TR, TD or TH elements in the DataTable, they will have a null - * entry in the array. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Get the data from the first row in the table - * var data = oTable._('tr:first'); - * - * // Do something useful with the data - * alert( "First cell is: "+data[0] ); - * } ); - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Filter to 'Webkit' and get all data for - * oTable.fnFilter('Webkit'); - * var data = oTable._('tr', {"search": "applied"}); - * - * // Do something with the data - * alert( data.length+" rows matched the search" ); - * } ); - */ - this._ = function ( sSelector, oOpts ) - { - return this.api(true).rows( sSelector, oOpts ).data(); - }; - - - /** - * Create a DataTables Api instance, with the currently selected tables for - * the Api's context. - * @param {boolean} [traditional=false] Set the API instance's context to be - * only the table referred to by the `DataTable.ext.iApiIndex` option, as was - * used in the API presented by DataTables 1.9- (i.e. the traditional mode), - * or if all tables captured in the jQuery object should be used. - * @return {DataTables.Api} - */ - this.api = function ( traditional ) - { - return traditional ? - new _Api( - _fnSettingsFromNode( this[ _ext.iApiIndex ] ) - ) : - new _Api( this ); - }; - - - /** - * Add a single new row or multiple rows of data to the table. Please note - * that this is suitable for client-side processing only - if you are using - * server-side processing (i.e. "bServerSide": true), then to add data, you - * must add it to the data source, i.e. the server-side, through an Ajax call. - * @param {array|object} data The data to be added to the table. This can be: - * <ul> - * <li>1D array of data - add a single row with the data provided</li> - * <li>2D array of arrays - add multiple rows in a single call</li> - * <li>object - data object when using <i>mData</i></li> - * <li>array of objects - multiple data objects when using <i>mData</i></li> - * </ul> - * @param {bool} [redraw=true] redraw the table or not - * @returns {array} An array of integers, representing the list of indexes in - * <i>aoData</i> ({@link DataTable.models.oSettings}) that have been added to - * the table. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * // Global var for counter - * var giCount = 2; - * - * $(document).ready(function() { - * $('#example').dataTable(); - * } ); - * - * function fnClickAddRow() { - * $('#example').dataTable().fnAddData( [ - * giCount+".1", - * giCount+".2", - * giCount+".3", - * giCount+".4" ] - * ); - * - * giCount++; - * } - */ - this.fnAddData = function( data, redraw ) - { - var api = this.api( true ); - - /* Check if we want to add multiple rows or not */ - var rows = Array.isArray(data) && ( Array.isArray(data[0]) || $.isPlainObject(data[0]) ) ? - api.rows.add( data ) : - api.row.add( data ); - - if ( redraw === undefined || redraw ) { - api.draw(); - } - - return rows.flatten().toArray(); - }; - - - /** - * This function will make DataTables recalculate the column sizes, based on the data - * contained in the table and the sizes applied to the columns (in the DOM, CSS or - * through the sWidth parameter). This can be useful when the width of the table's - * parent element changes (for example a window resize). - * @param {boolean} [bRedraw=true] Redraw the table or not, you will typically want to - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable( { - * "sScrollY": "200px", - * "bPaginate": false - * } ); - * - * $(window).on('resize', function () { - * oTable.fnAdjustColumnSizing(); - * } ); - * } ); - */ - this.fnAdjustColumnSizing = function ( bRedraw ) - { - var api = this.api( true ).columns.adjust(); - var settings = api.settings()[0]; - var scroll = settings.oScroll; - - if ( bRedraw === undefined || bRedraw ) { - api.draw( false ); - } - else if ( scroll.sX !== "" || scroll.sY !== "" ) { - /* If not redrawing, but scrolling, we want to apply the new column sizes anyway */ - _fnScrollDraw( settings ); - } - }; - - - /** - * Quickly and simply clear a table - * @param {bool} [bRedraw=true] redraw the table or not - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Immediately 'nuke' the current rows (perhaps waiting for an Ajax callback...) - * oTable.fnClearTable(); - * } ); - */ - this.fnClearTable = function( bRedraw ) - { - var api = this.api( true ).clear(); - - if ( bRedraw === undefined || bRedraw ) { - api.draw(); - } - }; - - - /** - * The exact opposite of 'opening' a row, this function will close any rows which - * are currently 'open'. - * @param {node} nTr the table row to 'close' - * @returns {int} 0 on success, or 1 if failed (can't find the row) - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable; - * - * // 'open' an information row when a row is clicked on - * $('#example tbody tr').click( function () { - * if ( oTable.fnIsOpen(this) ) { - * oTable.fnClose( this ); - * } else { - * oTable.fnOpen( this, "Temporary row opened", "info_row" ); - * } - * } ); - * - * oTable = $('#example').dataTable(); - * } ); - */ - this.fnClose = function( nTr ) + var _that = this; + var emptyInit = options === undefined; + var len = this.length; + + if ( emptyInit ) { + options = {}; + } + + // Method to get DT API instance from jQuery object + this.api = function () { - this.api( true ).row( nTr ).child.hide(); + return new _Api( this ); }; - - - /** - * Remove a row for the table - * @param {mixed} target The index of the row from aoData to be deleted, or - * the TR element you want to delete - * @param {function|null} [callBack] Callback function - * @param {bool} [redraw=true] Redraw the table or not - * @returns {array} The row that was deleted - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Immediately remove the first row - * oTable.fnDeleteRow( 0 ); - * } ); - */ - this.fnDeleteRow = function( target, callback, redraw ) - { - var api = this.api( true ); - var rows = api.rows( target ); - var settings = rows.settings()[0]; - var data = settings.aoData[ rows[0][0] ]; - - rows.remove(); - - if ( callback ) { - callback.call( this, settings, data ); + + this.each(function() { + // For each initialisation we want to give it a clean initialisation + // object that can be bashed around + var o = {}; + var oInit = len > 1 ? // optimisation for single table case + _fnExtend( o, options, true ) : + options; + + + var i=0, iLen; + var sId = this.getAttribute( 'id' ); + var bInitHandedOff = false; + var defaults = DataTable.defaults; + var $this = $(this); + + + /* Sanity check */ + if ( this.nodeName.toLowerCase() != 'table' ) + { + _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 ); + return; } - - if ( redraw === undefined || redraw ) { - api.draw(); + + $(this).trigger( 'options.dt', oInit ); + + /* Backwards compatibility for the defaults */ + _fnCompatOpts( defaults ); + _fnCompatCols( defaults.column ); + + /* Convert the camel-case defaults to Hungarian */ + _fnCamelToHungarian( defaults, defaults, true ); + _fnCamelToHungarian( defaults.column, defaults.column, true ); + + /* Setting up the initialisation object */ + _fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ), true ); + + + + /* Check to see if we are re-initialising a table */ + var allSettings = DataTable.settings; + for ( i=0, iLen=allSettings.length ; i<iLen ; i++ ) + { + var s = allSettings[i]; + + /* Base check on table node */ + if ( + s.nTable == this || + (s.nTHead && s.nTHead.parentNode == this) || + (s.nTFoot && s.nTFoot.parentNode == this) + ) { + var bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve; + var bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy; + + if ( emptyInit || bRetrieve ) + { + return s.oInstance; + } + else if ( bDestroy ) + { + new DataTable.Api(s).destroy(); + break; + } + else + { + _fnLog( s, 0, 'Cannot reinitialise DataTable', 3 ); + return; + } + } + + /* If the element we are initialising has the same ID as a table which was previously + * initialised, but the table nodes don't match (from before) then we destroy the old + * instance by simply deleting it. This is under the assumption that the table has been + * destroyed by other methods. Anyone using non-id selectors will need to do this manually + */ + if ( s.sTableId == this.id ) + { + allSettings.splice( i, 1 ); + break; + } } - - return data; - }; - - - /** - * Restore the table to it's original state in the DOM by removing all of DataTables - * enhancements, alterations to the DOM structure of the table and event listeners. - * @param {boolean} [remove=false] Completely remove the table from the DOM - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * // This example is fairly pointless in reality, but shows how fnDestroy can be used - * var oTable = $('#example').dataTable(); - * oTable.fnDestroy(); - * } ); - */ - this.fnDestroy = function ( remove ) - { - this.api( true ).destroy( remove ); - }; - - - /** - * Redraw the table - * @param {bool} [complete=true] Re-filter and resort (if enabled) the table before the draw. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Re-draw the table - you wouldn't want to do it here, but it's an example :-) - * oTable.fnDraw(); - * } ); - */ - this.fnDraw = function( complete ) - { - // Note that this isn't an exact match to the old call to _fnDraw - it takes - // into account the new data, but can hold position. - this.api( true ).draw( complete ); - }; - - - /** - * Filter the input based on data - * @param {string} sInput String to filter the table on - * @param {int|null} [iColumn] Column to limit filtering to - * @param {bool} [bRegex=false] Treat as regular expression or not - * @param {bool} [bSmart=true] Perform smart filtering or not - * @param {bool} [bShowGlobal=true] Show the input global filter in it's input box(es) - * @param {bool} [bCaseInsensitive=true] Do case-insensitive matching (true) or not (false) - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Sometime later - filter... - * oTable.fnFilter( 'test string' ); - * } ); - */ - this.fnFilter = function( sInput, iColumn, bRegex, bSmart, bShowGlobal, bCaseInsensitive ) - { - var api = this.api( true ); - - if ( iColumn === null || iColumn === undefined ) { - api.search( sInput, bRegex, bSmart, bCaseInsensitive ); + + /* Ensure the table has an ID - required for accessibility */ + if ( sId === null || sId === "" ) + { + sId = "DataTables_Table_"+(DataTable.ext._unique++); + this.id = sId; } - else { - api.column( iColumn ).search( sInput, bRegex, bSmart, bCaseInsensitive ); + + /* Create the settings object for this table and set some of the default parameters */ + var oSettings = $.extend( true, {}, DataTable.models.oSettings, { + "sDestroyWidth": $this[0].style.width, + "sInstance": sId, + "sTableId": sId, + colgroup: $('<colgroup>').prependTo(this), + fastData: function (row, column, type) { + return _fnGetCellData(oSettings, row, column, type); + } + } ); + oSettings.nTable = this; + oSettings.oInit = oInit; + + allSettings.push( oSettings ); + + // Make a single API instance available for internal handling + oSettings.api = new _Api( oSettings ); + + // Need to add the instance after the instance after the settings object has been added + // to the settings array, so we can self reference the table instance if more than one + oSettings.oInstance = (_that.length===1) ? _that : $this.dataTable(); + + // Backwards compatibility, before we apply all the defaults + _fnCompatOpts( oInit ); + + // If the length menu is given, but the init display length is not, use the length menu + if ( oInit.aLengthMenu && ! oInit.iDisplayLength ) + { + oInit.iDisplayLength = Array.isArray(oInit.aLengthMenu[0]) + ? oInit.aLengthMenu[0][0] + : $.isPlainObject( oInit.aLengthMenu[0] ) + ? oInit.aLengthMenu[0].value + : oInit.aLengthMenu[0]; } - - api.draw(); - }; - - - /** - * Get the data for the whole table, an individual row or an individual cell based on the - * provided parameters. - * @param {int|node} [src] A TR row node, TD/TH cell node or an integer. If given as - * a TR node then the data source for the whole row will be returned. If given as a - * TD/TH cell node then iCol will be automatically calculated and the data for the - * cell returned. If given as an integer, then this is treated as the aoData internal - * data index for the row (see fnGetPosition) and the data for that row used. - * @param {int} [col] Optional column index that you want the data of. - * @returns {array|object|string} If mRow is undefined, then the data for all rows is - * returned. If mRow is defined, just data for that row, and is iCol is - * defined, only data for the designated cell is returned. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * // Row data - * $(document).ready(function() { - * oTable = $('#example').dataTable(); - * - * oTable.$('tr').click( function () { - * var data = oTable.fnGetData( this ); - * // ... do something with the array / object of data for the row - * } ); - * } ); - * - * @example - * // Individual cell data - * $(document).ready(function() { - * oTable = $('#example').dataTable(); - * - * oTable.$('td').click( function () { - * var sData = oTable.fnGetData( this ); - * alert( 'The cell clicked on had the value of '+sData ); - * } ); - * } ); - */ - this.fnGetData = function( src, col ) - { - var api = this.api( true ); - - if ( src !== undefined ) { - var type = src.nodeName ? src.nodeName.toLowerCase() : ''; - - return col !== undefined || type == 'td' || type == 'th' ? - api.cell( src, col ).data() : - api.row( src ).data() || null; - } - - return api.data().toArray(); - }; - - - /** - * Get an array of the TR nodes that are used in the table's body. Note that you will - * typically want to use the '$' API method in preference to this as it is more - * flexible. - * @param {int} [iRow] Optional row index for the TR element you want - * @returns {array|node} If iRow is undefined, returns an array of all TR elements - * in the table's body, or iRow is defined, just the TR element requested. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Get the nodes from the table - * var nNodes = oTable.fnGetNodes( ); - * } ); - */ - this.fnGetNodes = function( iRow ) - { - var api = this.api( true ); - - return iRow !== undefined ? - api.row( iRow ).node() : - api.rows().nodes().flatten().toArray(); - }; - - - /** - * Get the array indexes of a particular cell from it's DOM element - * and column index including hidden columns - * @param {node} node this can either be a TR, TD or TH in the table's body - * @returns {int} If nNode is given as a TR, then a single index is returned, or - * if given as a cell, an array of [row index, column index (visible), - * column index (all)] is given. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * $('#example tbody td').click( function () { - * // Get the position of the current data from the node - * var aPos = oTable.fnGetPosition( this ); - * - * // Get the data array for this row - * var aData = oTable.fnGetData( aPos[0] ); - * - * // Update the data array and return the value - * aData[ aPos[1] ] = 'clicked'; - * this.innerHTML = 'clicked'; - * } ); - * - * // Init DataTables - * oTable = $('#example').dataTable(); - * } ); - */ - this.fnGetPosition = function( node ) - { - var api = this.api( true ); - var nodeName = node.nodeName.toUpperCase(); - - if ( nodeName == 'TR' ) { - return api.row( node ).index(); - } - else if ( nodeName == 'TD' || nodeName == 'TH' ) { - var cell = api.cell( node ).index(); - - return [ - cell.row, - cell.columnVisible, - cell.column - ]; - } - return null; - }; - - - /** - * Check to see if a row is 'open' or not. - * @param {node} nTr the table row to check - * @returns {boolean} true if the row is currently open, false otherwise - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable; - * - * // 'open' an information row when a row is clicked on - * $('#example tbody tr').click( function () { - * if ( oTable.fnIsOpen(this) ) { - * oTable.fnClose( this ); - * } else { - * oTable.fnOpen( this, "Temporary row opened", "info_row" ); - * } - * } ); - * - * oTable = $('#example').dataTable(); - * } ); - */ - this.fnIsOpen = function( nTr ) - { - return this.api( true ).row( nTr ).child.isShown(); - }; - - - /** - * This function will place a new row directly after a row which is currently - * on display on the page, with the HTML contents that is passed into the - * function. This can be used, for example, to ask for confirmation that a - * particular record should be deleted. - * @param {node} nTr The table row to 'open' - * @param {string|node|jQuery} mHtml The HTML to put into the row - * @param {string} sClass Class to give the new TD cell - * @returns {node} The row opened. Note that if the table row passed in as the - * first parameter, is not found in the table, this method will silently - * return. - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable; - * - * // 'open' an information row when a row is clicked on - * $('#example tbody tr').click( function () { - * if ( oTable.fnIsOpen(this) ) { - * oTable.fnClose( this ); - * } else { - * oTable.fnOpen( this, "Temporary row opened", "info_row" ); - * } - * } ); - * - * oTable = $('#example').dataTable(); - * } ); - */ - this.fnOpen = function( nTr, mHtml, sClass ) - { - return this.api( true ) - .row( nTr ) - .child( mHtml, sClass ) - .show() - .child()[0]; - }; - - - /** - * Change the pagination - provides the internal logic for pagination in a simple API - * function. With this function you can have a DataTables table go to the next, - * previous, first or last pages. - * @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last" - * or page number to jump to (integer), note that page 0 is the first page. - * @param {bool} [bRedraw=true] Redraw the table or not - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * oTable.fnPageChange( 'next' ); - * } ); - */ - this.fnPageChange = function ( mAction, bRedraw ) - { - var api = this.api( true ).page( mAction ); - - if ( bRedraw === undefined || bRedraw ) { - api.draw(false); - } - }; - - - /** - * Show a particular column - * @param {int} iCol The column whose display should be changed - * @param {bool} bShow Show (true) or hide (false) the column - * @param {bool} [bRedraw=true] Redraw the table or not - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Hide the second column after initialisation - * oTable.fnSetColumnVis( 1, false ); - * } ); - */ - this.fnSetColumnVis = function ( iCol, bShow, bRedraw ) - { - var api = this.api( true ).column( iCol ).visible( bShow ); - - if ( bRedraw === undefined || bRedraw ) { - api.columns.adjust().draw(); - } - }; - - - /** - * Get the settings for a particular table for external manipulation - * @returns {object} DataTables settings object. See - * {@link DataTable.models.oSettings} - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * var oSettings = oTable.fnSettings(); - * - * // Show an example parameter from the settings - * alert( oSettings._iDisplayStart ); - * } ); - */ - this.fnSettings = function() - { - return _fnSettingsFromNode( this[_ext.iApiIndex] ); - }; - - - /** - * Sort the table by a particular column - * @param {int} iCol the data index to sort on. Note that this will not match the - * 'display index' if you have hidden data entries - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Sort immediately with columns 0 and 1 - * oTable.fnSort( [ [0,'asc'], [1,'asc'] ] ); - * } ); - */ - this.fnSort = function( aaSort ) - { - this.api( true ).order( aaSort ).draw(); - }; - - - /** - * Attach a sort listener to an element for a given column - * @param {node} nNode the element to attach the sort listener to - * @param {int} iColumn the column that a click on this node will sort on - * @param {function} [fnCallback] callback function when sort is run - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * - * // Sort on column 1, when 'sorter' is clicked on - * oTable.fnSortListener( document.getElementById('sorter'), 1 ); - * } ); - */ - this.fnSortListener = function( nNode, iColumn, fnCallback ) - { - this.api( true ).order.listener( nNode, iColumn, fnCallback ); - }; - - - /** - * Update a table cell or row - this method will accept either a single value to - * update the cell with, an array of values with one element for each column or - * an object in the same format as the original data source. The function is - * self-referencing in order to make the multi column updates easier. - * @param {object|array|string} mData Data to update the cell/row with - * @param {node|int} mRow TR element you want to update or the aoData index - * @param {int} [iColumn] The column to update, give as null or undefined to - * update a whole row. - * @param {bool} [bRedraw=true] Redraw the table or not - * @param {bool} [bAction=true] Perform pre-draw actions or not - * @returns {int} 0 on success, 1 on error - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * oTable.fnUpdate( 'Example update', 0, 0 ); // Single cell - * oTable.fnUpdate( ['a', 'b', 'c', 'd', 'e'], $('tbody tr')[0] ); // Row - * } ); - */ - this.fnUpdate = function( mData, mRow, iColumn, bRedraw, bAction ) - { - var api = this.api( true ); - - if ( iColumn === undefined || iColumn === null ) { - api.row( mRow ).data( mData ); - } - else { - api.cell( mRow, iColumn ).data( mData ); - } - - if ( bAction === undefined || bAction ) { - api.columns.adjust(); - } - - if ( bRedraw === undefined || bRedraw ) { - api.draw(); - } - return 0; - }; - - - /** - * Provide a common method for plug-ins to check the version of DataTables being used, in order - * to ensure compatibility. - * @param {string} sVersion Version string to check for, in the format "X.Y.Z". Note that the - * formats "X" and "X.Y" are also acceptable. - * @returns {boolean} true if this version of DataTables is greater or equal to the required - * version, or false if this version of DataTales is not suitable - * @method - * @dtopt API - * @deprecated Since v1.10 - * - * @example - * $(document).ready(function() { - * var oTable = $('#example').dataTable(); - * alert( oTable.fnVersionCheck( '1.9.0' ) ); - * } ); - */ - this.fnVersionCheck = _ext.fnVersionCheck; - - - var _that = this; - var emptyInit = options === undefined; - var len = this.length; - - if ( emptyInit ) { - options = {}; - } - - this.oApi = this.internal = _ext.internal; - - // Extend with old style plug-in API methods - for ( var fn in DataTable.ext.internal ) { - if ( fn ) { - this[fn] = _fnExternApiFunc(fn); - } - } - - this.each(function() { - // For each initialisation we want to give it a clean initialisation - // object that can be bashed around - var o = {}; - var oInit = len > 1 ? // optimisation for single table case - _fnExtend( o, options, true ) : - options; - - /*global oInit,_that,emptyInit*/ - var i=0, iLen, j, jLen, k, kLen; - var sId = this.getAttribute( 'id' ); - var bInitHandedOff = false; - var defaults = DataTable.defaults; - var $this = $(this); + // Apply the defaults and init options to make a single init object will all + // options defined from defaults and instance options. + oInit = _fnExtend( $.extend( true, {}, defaults ), oInit ); - /* Sanity check */ - if ( this.nodeName.toLowerCase() != 'table' ) - { - _fnLog( null, 0, 'Non-table node initialisation ('+this.nodeName+')', 2 ); - return; - } - /* Backwards compatibility for the defaults */ - _fnCompatOpts( defaults ); - _fnCompatCols( defaults.column ); + // Map the initialisation options onto the settings object + _fnMap( oSettings.oFeatures, oInit, [ + "bPaginate", + "bLengthChange", + "bFilter", + "bSort", + "bSortMulti", + "bInfo", + "bProcessing", + "bAutoWidth", + "bSortClasses", + "bServerSide", + "bDeferRender" + ] ); + _fnMap( oSettings, oInit, [ + "ajax", + "fnFormatNumber", + "sServerMethod", + "aaSorting", + "aaSortingFixed", + "aLengthMenu", + "sPaginationType", + "iStateDuration", + "bSortCellsTop", + "iTabIndex", + "sDom", + "fnStateLoadCallback", + "fnStateSaveCallback", + "renderer", + "searchDelay", + "rowId", + "caption", + "layout", + [ "iCookieDuration", "iStateDuration" ], // backwards compat + [ "oSearch", "oPreviousSearch" ], + [ "aoSearchCols", "aoPreSearchCols" ], + [ "iDisplayLength", "_iDisplayLength" ] + ] ); + _fnMap( oSettings.oScroll, oInit, [ + [ "sScrollX", "sX" ], + [ "sScrollXInner", "sXInner" ], + [ "sScrollY", "sY" ], + [ "bScrollCollapse", "bCollapse" ] + ] ); + _fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" ); - /* Convert the camel-case defaults to Hungarian */ - _fnCamelToHungarian( defaults, defaults, true ); - _fnCamelToHungarian( defaults.column, defaults.column, true ); + /* Callback functions which are array driven */ + _fnCallbackReg( oSettings, 'aoDrawCallback', oInit.fnDrawCallback ); + _fnCallbackReg( oSettings, 'aoStateSaveParams', oInit.fnStateSaveParams ); + _fnCallbackReg( oSettings, 'aoStateLoadParams', oInit.fnStateLoadParams ); + _fnCallbackReg( oSettings, 'aoStateLoaded', oInit.fnStateLoaded ); + _fnCallbackReg( oSettings, 'aoRowCallback', oInit.fnRowCallback ); + _fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow ); + _fnCallbackReg( oSettings, 'aoHeaderCallback', oInit.fnHeaderCallback ); + _fnCallbackReg( oSettings, 'aoFooterCallback', oInit.fnFooterCallback ); + _fnCallbackReg( oSettings, 'aoInitComplete', oInit.fnInitComplete ); + _fnCallbackReg( oSettings, 'aoPreDrawCallback', oInit.fnPreDrawCallback ); - /* Setting up the initialisation object */ - _fnCamelToHungarian( defaults, $.extend( oInit, $this.data() ), true ); + oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId ); + /* Browser support detection */ + _fnBrowserDetect( oSettings ); + var oClasses = oSettings.oClasses; - /* Check to see if we are re-initialising a table */ - var allSettings = DataTable.settings; - for ( i=0, iLen=allSettings.length ; i<iLen ; i++ ) - { - var s = allSettings[i]; - - /* Base check on table node */ - if ( - s.nTable == this || - (s.nTHead && s.nTHead.parentNode == this) || - (s.nTFoot && s.nTFoot.parentNode == this) - ) { - var bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve; - var bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy; - - if ( emptyInit || bRetrieve ) - { - return s.oInstance; - } - else if ( bDestroy ) - { - s.oInstance.fnDestroy(); - break; - } - else - { - _fnLog( s, 0, 'Cannot reinitialise DataTable', 3 ); - return; - } - } - - /* If the element we are initialising has the same ID as a table which was previously - * initialised, but the table nodes don't match (from before) then we destroy the old - * instance by simply deleting it. This is under the assumption that the table has been - * destroyed by other methods. Anyone using non-id selectors will need to do this manually - */ - if ( s.sTableId == this.id ) - { - allSettings.splice( i, 1 ); - break; - } - } - - /* Ensure the table has an ID - required for accessibility */ - if ( sId === null || sId === "" ) - { - sId = "DataTables_Table_"+(DataTable.ext._unique++); - this.id = sId; - } - - /* Create the settings object for this table and set some of the default parameters */ - var oSettings = $.extend( true, {}, DataTable.models.oSettings, { - "sDestroyWidth": $this[0].style.width, - "sInstance": sId, - "sTableId": sId - } ); - oSettings.nTable = this; - oSettings.oApi = _that.internal; - oSettings.oInit = oInit; - - allSettings.push( oSettings ); - - // Need to add the instance after the instance after the settings object has been added - // to the settings array, so we can self reference the table instance if more than one - oSettings.oInstance = (_that.length===1) ? _that : $this.dataTable(); - - // Backwards compatibility, before we apply all the defaults - _fnCompatOpts( oInit ); - _fnLanguageCompat( oInit.oLanguage ); - - // If the length menu is given, but the init display length is not, use the length menu - if ( oInit.aLengthMenu && ! oInit.iDisplayLength ) - { - oInit.iDisplayLength = Array.isArray( oInit.aLengthMenu[0] ) ? - oInit.aLengthMenu[0][0] : oInit.aLengthMenu[0]; - } - - // Apply the defaults and init options to make a single init object will all - // options defined from defaults and instance options. - oInit = _fnExtend( $.extend( true, {}, defaults ), oInit ); - - - // Map the initialisation options onto the settings object - _fnMap( oSettings.oFeatures, oInit, [ - "bPaginate", - "bLengthChange", - "bFilter", - "bSort", - "bSortMulti", - "bInfo", - "bProcessing", - "bAutoWidth", - "bSortClasses", - "bServerSide", - "bDeferRender" - ] ); - _fnMap( oSettings, oInit, [ - "asStripeClasses", - "ajax", - "fnServerData", - "fnFormatNumber", - "sServerMethod", - "aaSorting", - "aaSortingFixed", - "aLengthMenu", - "sPaginationType", - "sAjaxSource", - "sAjaxDataProp", - "iStateDuration", - "sDom", - "bSortCellsTop", - "iTabIndex", - "fnStateLoadCallback", - "fnStateSaveCallback", - "renderer", - "searchDelay", - "rowId", - [ "iCookieDuration", "iStateDuration" ], // backwards compat - [ "oSearch", "oPreviousSearch" ], - [ "aoSearchCols", "aoPreSearchCols" ], - [ "iDisplayLength", "_iDisplayLength" ] - ] ); - _fnMap( oSettings.oScroll, oInit, [ - [ "sScrollX", "sX" ], - [ "sScrollXInner", "sXInner" ], - [ "sScrollY", "sY" ], - [ "bScrollCollapse", "bCollapse" ] - ] ); - _fnMap( oSettings.oLanguage, oInit, "fnInfoCallback" ); - - /* Callback functions which are array driven */ - _fnCallbackReg( oSettings, 'aoDrawCallback', oInit.fnDrawCallback, 'user' ); - _fnCallbackReg( oSettings, 'aoServerParams', oInit.fnServerParams, 'user' ); - _fnCallbackReg( oSettings, 'aoStateSaveParams', oInit.fnStateSaveParams, 'user' ); - _fnCallbackReg( oSettings, 'aoStateLoadParams', oInit.fnStateLoadParams, 'user' ); - _fnCallbackReg( oSettings, 'aoStateLoaded', oInit.fnStateLoaded, 'user' ); - _fnCallbackReg( oSettings, 'aoRowCallback', oInit.fnRowCallback, 'user' ); - _fnCallbackReg( oSettings, 'aoRowCreatedCallback', oInit.fnCreatedRow, 'user' ); - _fnCallbackReg( oSettings, 'aoHeaderCallback', oInit.fnHeaderCallback, 'user' ); - _fnCallbackReg( oSettings, 'aoFooterCallback', oInit.fnFooterCallback, 'user' ); - _fnCallbackReg( oSettings, 'aoInitComplete', oInit.fnInitComplete, 'user' ); - _fnCallbackReg( oSettings, 'aoPreDrawCallback', oInit.fnPreDrawCallback, 'user' ); - - oSettings.rowIdFn = _fnGetObjectDataFn( oInit.rowId ); - - /* Browser support detection */ - _fnBrowserDetect( oSettings ); - - var oClasses = oSettings.oClasses; - - $.extend( oClasses, DataTable.ext.classes, oInit.oClasses ); - $this.addClass( oClasses.sTable ); + $.extend( oClasses, DataTable.ext.classes, oInit.oClasses ); + $this.addClass( oClasses.table ); + if (! oSettings.oFeatures.bPaginate) { + oInit.iDisplayStart = 0; + } if ( oSettings.iInitDisplayStart === undefined ) { @@ -1065,14 +312,6 @@ oSettings._iDisplayStart = oInit.iDisplayStart; } - if ( oInit.iDeferLoading !== null ) - { - oSettings.bDeferLoading = true; - var tmp = Array.isArray( oInit.iDeferLoading ); - oSettings._iRecordsDisplay = tmp ? oInit.iDeferLoading[0] : oInit.iDeferLoading; - oSettings._iRecordsTotal = tmp ? oInit.iDeferLoading[1] : oInit.iDeferLoading; - } - /* Language definitions */ var oLanguage = oSettings.oLanguage; $.extend( true, oLanguage, oInit.oLanguage ); @@ -1088,14 +327,16 @@ url: oLanguage.sUrl, success: function ( json ) { _fnCamelToHungarian( defaults.oLanguage, json ); - _fnLanguageCompat( json ); $.extend( true, oLanguage, json, oSettings.oInit.oLanguage ); - _fnCallbackFire( oSettings, null, 'i18n', [oSettings]); + _fnCallbackFire( oSettings, null, 'i18n', [oSettings], true); _fnInitialise( oSettings ); }, error: function () { - // Error occurred loading language file, continue on as best we can + // Error occurred loading language file + _fnLog( oSettings, 0, 'i18n file loading error', 21 ); + + // continue on as best we can _fnInitialise( oSettings ); } } ); @@ -1106,67 +347,38 @@ } /* - * Stripes - */ - if ( oInit.asStripeClasses === null ) - { - oSettings.asStripeClasses =[ - oClasses.sStripeOdd, - oClasses.sStripeEven - ]; - } - - /* Remove row stripe classes if they are already on the table row */ - var stripeClasses = oSettings.asStripeClasses; - var rowOne = $this.children('tbody').find('tr').eq(0); - if ( $.inArray( true, $.map( stripeClasses, function(el, i) { - return rowOne.hasClass(el); - } ) ) !== -1 ) { - $('tbody tr', this).removeClass( stripeClasses.join(' ') ); - oSettings.asDestroyStripes = stripeClasses.slice(); - } - - /* * Columns * See if we should load columns automatically or use defined ones */ - var anThs = []; - var aoColumnsInit; - var nThead = this.getElementsByTagName('thead'); - if ( nThead.length !== 0 ) - { - _fnDetectHeader( oSettings.aoHeader, nThead[0] ); - anThs = _fnGetUniqueThs( oSettings ); - } + var columnsInit = []; + var thead = this.getElementsByTagName('thead'); + var initHeaderLayout = _fnDetectHeader( oSettings, thead[0] ); - /* If not given a column array, generate one with nulls */ - if ( oInit.aoColumns === null ) - { - aoColumnsInit = []; - for ( i=0, iLen=anThs.length ; i<iLen ; i++ ) - { - aoColumnsInit.push( null ); - } + // If we don't have a columns array, then generate one with nulls + if ( oInit.aoColumns ) { + columnsInit = oInit.aoColumns; } - else - { - aoColumnsInit = oInit.aoColumns; + else if ( initHeaderLayout.length ) { + for ( i=0, iLen=initHeaderLayout[0].length ; i<iLen ; i++ ) { + columnsInit.push( null ); + } } - /* Add the columns */ - for ( i=0, iLen=aoColumnsInit.length ; i<iLen ; i++ ) - { - _fnAddColumn( oSettings, anThs ? anThs[i] : null ); + // Add the columns + for ( i=0, iLen=columnsInit.length ; i<iLen ; i++ ) { + _fnAddColumn( oSettings ); } - /* Apply the column definitions */ - _fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, aoColumnsInit, function (iCol, oDef) { + // Apply the column definitions + _fnApplyColumnDefs( oSettings, oInit.aoColumnDefs, columnsInit, initHeaderLayout, function (iCol, oDef) { _fnColumnOptions( oSettings, iCol, oDef ); } ); /* HTML5 attribute detection - build an mData object automatically if the * attributes are found */ + var rowOne = $this.children('tbody').find('tr').eq(0); + if ( rowOne.length ) { var a = function ( cell, name ) { return cell.getAttribute( 'data-'+name ) !== null ? name : null; @@ -1219,44 +431,39 @@ */ _fnSortingClasses( oSettings ); - if ( features.bSort ) { - _fnCallbackReg( oSettings, 'aoDrawCallback', function () { - if ( oSettings.bSorted ) { - var aSort = _fnSortFlatten( oSettings ); - var sortedColumns = {}; - - $.each( aSort, function (i, val) { - sortedColumns[ val.src ] = val.dir; - } ); - - _fnCallbackFire( oSettings, null, 'order', [oSettings, aSort, sortedColumns] ); - _fnSortAria( oSettings ); - } - } ); - } - _fnCallbackReg( oSettings, 'aoDrawCallback', function () { if ( oSettings.bSorted || _fnDataSource( oSettings ) === 'ssp' || features.bDeferRender ) { _fnSortingClasses( oSettings ); } - }, 'sc' ); + } ); /* * Final init * Cache the header, body and footer as required, creating them if needed */ + var caption = $this.children('caption'); - // Work around for Webkit bug 83867 - store the caption-side before removing from doc - var captions = $this.children('caption').each( function () { - this._captionSide = $(this).css('caption-side'); - } ); + if ( oSettings.caption ) { + if ( caption.length === 0 ) { + caption = $('<caption/>').appendTo( $this ); + } + + caption.html( oSettings.caption ); + } + + // Store the caption side, so we can remove the element from the document + // when creating the element + if (caption.length) { + caption[0]._captionSide = caption.css('caption-side'); + oSettings.captionNode = caption[0]; + } - var thead = $this.children('thead'); if ( thead.length === 0 ) { thead = $('<thead/>').appendTo($this); } oSettings.nTHead = thead[0]; + $('tr', thead).addClass(oClasses.thead.row); var tbody = $this.children('tbody'); if ( tbody.length === 0 ) { @@ -1265,31 +472,22 @@ oSettings.nTBody = tbody[0]; var tfoot = $this.children('tfoot'); - if ( tfoot.length === 0 && captions.length > 0 && (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) { + if ( tfoot.length === 0 ) { // If we are a scrolling table, and no footer has been given, then we need to create // a tfoot element for the caption element to be appended to tfoot = $('<tfoot/>').appendTo($this); } + oSettings.nTFoot = tfoot[0]; + $('tr', tfoot).addClass(oClasses.tfoot.row); - if ( tfoot.length === 0 || tfoot.children().length === 0 ) { - $this.addClass( oClasses.sNoFooter ); - } - else if ( tfoot.length > 0 ) { - oSettings.nTFoot = tfoot[0]; - _fnDetectHeader( oSettings.aoFooter, oSettings.nTFoot ); - } - - /* Check if there is data passing into the constructor */ + // Check if there is data passing into the constructor if ( oInit.aaData ) { for ( i=0 ; i<oInit.aaData.length ; i++ ) { _fnAddData( oSettings, oInit.aaData[ i ] ); } } - else if ( oSettings.bDeferLoading || _fnDataSource( oSettings ) == 'dom' ) { - /* Grab the data from the page - only do this when deferred loading or no Ajax - * source since there is no point in reading the DOM data if we are then going - * to replace it with Ajax data - */ + else if ( _fnDataSource( oSettings ) == 'dom' ) { + // Grab the data from the page _fnAddTr( oSettings, $(oSettings.nTBody).children('tr') ); } @@ -1308,7 +506,7 @@ }; /* Must be done after everything which can be overridden by the state saving! */ - _fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' ); + _fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState ); if ( oInit.bStateSave ) { @@ -1325,6 +523,569 @@ }; + + /** + * DataTables extensions + * + * This namespace acts as a collection area for plug-ins that can be used to + * extend DataTables capabilities. Indeed many of the build in methods + * use this method to provide their own capabilities (sorting methods for + * example). + * + * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy + * reasons + * + * @namespace + */ + DataTable.ext = _ext = { + /** + * Buttons. For use with the Buttons extension for DataTables. This is + * defined here so other extensions can define buttons regardless of load + * order. It is _not_ used by DataTables core. + * + * @type object + * @default {} + */ + buttons: {}, + + + /** + * Element class names + * + * @type object + * @default {} + */ + classes: {}, + + + /** + * DataTables build type (expanded by the download builder) + * + * @type string + */ + builder: "bs5/dt-2.0.8", + + + /** + * Error reporting. + * + * How should DataTables report an error. Can take the value 'alert', + * 'throw', 'none' or a function. + * + * @type string|function + * @default alert + */ + errMode: "alert", + + + /** + * Legacy so v1 plug-ins don't throw js errors on load + */ + feature: [], + + /** + * Feature plug-ins. + * + * This is an object of callbacks which provide the features for DataTables + * to be initialised via the `layout` option. + */ + features: {}, + + + /** + * Row searching. + * + * This method of searching is complimentary to the default type based + * searching, and a lot more comprehensive as it allows you complete control + * over the searching logic. Each element in this array is a function + * (parameters described below) that is called for every row in the table, + * and your logic decides if it should be included in the searching data set + * or not. + * + * Searching functions have the following input parameters: + * + * 1. `{object}` DataTables settings object: see + * {@link DataTable.models.oSettings} + * 2. `{array|object}` Data for the row to be processed (same as the + * original format that was passed in as the data source, or an array + * from a DOM data source + * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which + * can be useful to retrieve the `TR` element if you need DOM interaction. + * + * And the following return is expected: + * + * * {boolean} Include the row in the searched result set (true) or not + * (false) + * + * Note that as with the main search ability in DataTables, technically this + * is "filtering", since it is subtractive. However, for consistency in + * naming we call it searching here. + * + * @type array + * @default [] + * + * @example + * // The following example shows custom search being applied to the + * // fourth column (i.e. the data[3] index) based on two input values + * // from the end-user, matching the data in a certain range. + * $.fn.dataTable.ext.search.push( + * function( settings, data, dataIndex ) { + * var min = document.getElementById('min').value * 1; + * var max = document.getElementById('max').value * 1; + * var version = data[3] == "-" ? 0 : data[3]*1; + * + * if ( min == "" && max == "" ) { + * return true; + * } + * else if ( min == "" && version < max ) { + * return true; + * } + * else if ( min < version && "" == max ) { + * return true; + * } + * else if ( min < version && version < max ) { + * return true; + * } + * return false; + * } + * ); + */ + search: [], + + + /** + * Selector extensions + * + * The `selector` option can be used to extend the options available for the + * selector modifier options (`selector-modifier` object data type) that + * each of the three built in selector types offer (row, column and cell + + * their plural counterparts). For example the Select extension uses this + * mechanism to provide an option to select only rows, columns and cells + * that have been marked as selected by the end user (`{selected: true}`), + * which can be used in conjunction with the existing built in selector + * options. + * + * Each property is an array to which functions can be pushed. The functions + * take three attributes: + * + * * Settings object for the host table + * * Options object (`selector-modifier` object type) + * * Array of selected item indexes + * + * The return is an array of the resulting item indexes after the custom + * selector has been applied. + * + * @type object + */ + selector: { + cell: [], + column: [], + row: [] + }, + + + /** + * Legacy configuration options. Enable and disable legacy options that + * are available in DataTables. + * + * @type object + */ + legacy: { + /** + * Enable / disable DataTables 1.9 compatible server-side processing + * requests + * + * @type boolean + * @default null + */ + ajax: null + }, + + + /** + * Pagination plug-in methods. + * + * Each entry in this object is a function and defines which buttons should + * be shown by the pagination rendering method that is used for the table: + * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the + * buttons are displayed in the document, while the functions here tell it + * what buttons to display. This is done by returning an array of button + * descriptions (what each button will do). + * + * Pagination types (the four built in options and any additional plug-in + * options defined here) can be used through the `paginationType` + * initialisation parameter. + * + * The functions defined take two parameters: + * + * 1. `{int} page` The current page index + * 2. `{int} pages` The number of pages in the table + * + * Each function is expected to return an array where each element of the + * array can be one of: + * + * * `first` - Jump to first page when activated + * * `last` - Jump to last page when activated + * * `previous` - Show previous page when activated + * * `next` - Show next page when activated + * * `{int}` - Show page of the index given + * * `{array}` - A nested array containing the above elements to add a + * containing 'DIV' element (might be useful for styling). + * + * Note that DataTables v1.9- used this object slightly differently whereby + * an object with two functions would be defined for each plug-in. That + * ability is still supported by DataTables 1.10+ to provide backwards + * compatibility, but this option of use is now decremented and no longer + * documented in DataTables 1.10+. + * + * @type object + * @default {} + * + * @example + * // Show previous, next and current page buttons only + * $.fn.dataTableExt.oPagination.current = function ( page, pages ) { + * return [ 'previous', page, 'next' ]; + * }; + */ + pager: {}, + + + renderer: { + pageButton: {}, + header: {} + }, + + + /** + * Ordering plug-ins - custom data source + * + * The extension options for ordering of data available here is complimentary + * to the default type based ordering that DataTables typically uses. It + * allows much greater control over the the data that is being used to + * order a column, but is necessarily therefore more complex. + * + * This type of ordering is useful if you want to do ordering based on data + * live from the DOM (for example the contents of an 'input' element) rather + * than just the static string that DataTables knows of. + * + * The way these plug-ins work is that you create an array of the values you + * wish to be ordering for the column in question and then return that + * array. The data in the array much be in the index order of the rows in + * the table (not the currently ordering order!). Which order data gathering + * function is run here depends on the `dt-init columns.orderDataType` + * parameter that is used for the column (if any). + * + * The functions defined take two parameters: + * + * 1. `{object}` DataTables settings object: see + * {@link DataTable.models.oSettings} + * 2. `{int}` Target column index + * + * Each function is expected to return an array: + * + * * `{array}` Data for the column to be ordering upon + * + * @type array + * + * @example + * // Ordering using `input` node values + * $.fn.dataTable.ext.order['dom-text'] = function ( settings, col ) + * { + * return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) { + * return $('input', td).val(); + * } ); + * } + */ + order: {}, + + + /** + * Type based plug-ins. + * + * Each column in DataTables has a type assigned to it, either by automatic + * detection or by direct assignment using the `type` option for the column. + * The type of a column will effect how it is ordering and search (plug-ins + * can also make use of the column type if required). + * + * @namespace + */ + type: { + /** + * Automatic column class assignment + */ + className: {}, + + /** + * Type detection functions. + * + * The functions defined in this object are used to automatically detect + * a column's type, making initialisation of DataTables super easy, even + * when complex data is in the table. + * + * The functions defined take two parameters: + * + * 1. `{*}` Data from the column cell to be analysed + * 2. `{settings}` DataTables settings object. This can be used to + * perform context specific type detection - for example detection + * based on language settings such as using a comma for a decimal + * place. Generally speaking the options from the settings will not + * be required + * + * Each function is expected to return: + * + * * `{string|null}` Data type detected, or null if unknown (and thus + * pass it on to the other type detection functions. + * + * @type array + * + * @example + * // Currency type detection plug-in: + * $.fn.dataTable.ext.type.detect.push( + * function ( data, settings ) { + * // Check the numeric part + * if ( ! data.substring(1).match(/[0-9]/) ) { + * return null; + * } + * + * // Check prefixed by currency + * if ( data.charAt(0) == '$' || data.charAt(0) == '&pound;' ) { + * return 'currency'; + * } + * return null; + * } + * ); + */ + detect: [], + + /** + * Automatic renderer assignment + */ + render: {}, + + + /** + * Type based search formatting. + * + * The type based searching functions can be used to pre-format the + * data to be search on. For example, it can be used to strip HTML + * tags or to de-format telephone numbers for numeric only searching. + * + * Note that is a search is not defined for a column of a given type, + * no search formatting will be performed. + * + * Pre-processing of searching data plug-ins - When you assign the sType + * for a column (or have it automatically detected for you by DataTables + * or a type detection plug-in), you will typically be using this for + * custom sorting, but it can also be used to provide custom searching + * by allowing you to pre-processing the data and returning the data in + * the format that should be searched upon. This is done by adding + * functions this object with a parameter name which matches the sType + * for that target column. This is the corollary of <i>afnSortData</i> + * for searching data. + * + * The functions defined take a single parameter: + * + * 1. `{*}` Data from the column cell to be prepared for searching + * + * Each function is expected to return: + * + * * `{string|null}` Formatted string that will be used for the searching. + * + * @type object + * @default {} + * + * @example + * $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) { + * return d.replace(/\n/g," ").replace( /<.*?>/g, "" ); + * } + */ + search: {}, + + + /** + * Type based ordering. + * + * The column type tells DataTables what ordering to apply to the table + * when a column is sorted upon. The order for each type that is defined, + * is defined by the functions available in this object. + * + * Each ordering option can be described by three properties added to + * this object: + * + * * `{type}-pre` - Pre-formatting function + * * `{type}-asc` - Ascending order function + * * `{type}-desc` - Descending order function + * + * All three can be used together, only `{type}-pre` or only + * `{type}-asc` and `{type}-desc` together. It is generally recommended + * that only `{type}-pre` is used, as this provides the optimal + * implementation in terms of speed, although the others are provided + * for compatibility with existing Javascript sort functions. + * + * `{type}-pre`: Functions defined take a single parameter: + * + * 1. `{*}` Data from the column cell to be prepared for ordering + * + * And return: + * + * * `{*}` Data to be sorted upon + * + * `{type}-asc` and `{type}-desc`: Functions are typical Javascript sort + * functions, taking two parameters: + * + * 1. `{*}` Data to compare to the second parameter + * 2. `{*}` Data to compare to the first parameter + * + * And returning: + * + * * `{*}` Ordering match: <0 if first parameter should be sorted lower + * than the second parameter, ===0 if the two parameters are equal and + * >0 if the first parameter should be sorted height than the second + * parameter. + * + * @type object + * @default {} + * + * @example + * // Numeric ordering of formatted numbers with a pre-formatter + * $.extend( $.fn.dataTable.ext.type.order, { + * "string-pre": function(x) { + * a = (a === "-" || a === "") ? 0 : a.replace( /[^\d\-\.]/g, "" ); + * return parseFloat( a ); + * } + * } ); + * + * @example + * // Case-sensitive string ordering, with no pre-formatting method + * $.extend( $.fn.dataTable.ext.order, { + * "string-case-asc": function(x,y) { + * return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + * }, + * "string-case-desc": function(x,y) { + * return ((x < y) ? 1 : ((x > y) ? -1 : 0)); + * } + * } ); + */ + order: {} + }, + + /** + * Unique DataTables instance counter + * + * @type int + * @private + */ + _unique: 0, + + + // + // Depreciated + // The following properties are retained for backwards compatibility only. + // The should not be used in new projects and will be removed in a future + // version + // + + /** + * Version check function. + * @type function + * @depreciated Since 1.10 + */ + fnVersionCheck: DataTable.fnVersionCheck, + + + /** + * Index for what 'this' index API functions should use + * @type int + * @deprecated Since v1.10 + */ + iApiIndex: 0, + + + /** + * Software version + * @type string + * @deprecated Since v1.10 + */ + sVersion: DataTable.version + }; + + + // + // Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts + // + $.extend( _ext, { + afnFiltering: _ext.search, + aTypes: _ext.type.detect, + ofnSearch: _ext.type.search, + oSort: _ext.type.order, + afnSortData: _ext.order, + aoFeatures: _ext.feature, + oStdClasses: _ext.classes, + oPagination: _ext.pager + } ); + + + $.extend( DataTable.ext.classes, { + container: 'dt-container', + empty: { + row: 'dt-empty' + }, + info: { + container: 'dt-info' + }, + length: { + container: 'dt-length', + select: 'dt-input' + }, + order: { + canAsc: 'dt-orderable-asc', + canDesc: 'dt-orderable-desc', + isAsc: 'dt-ordering-asc', + isDesc: 'dt-ordering-desc', + none: 'dt-orderable-none', + position: 'sorting_' + }, + processing: { + container: 'dt-processing' + }, + scrolling: { + body: 'dt-scroll-body', + container: 'dt-scroll', + footer: { + self: 'dt-scroll-foot', + inner: 'dt-scroll-footInner' + }, + header: { + self: 'dt-scroll-head', + inner: 'dt-scroll-headInner' + } + }, + search: { + container: 'dt-search', + input: 'dt-input' + }, + table: 'dataTable', + tbody: { + cell: '', + row: '' + }, + thead: { + cell: '', + row: '' + }, + tfoot: { + cell: '', + row: '' + }, + paging: { + active: 'current', + button: 'dt-paging-button', + container: 'dt-paging', + disabled: 'disabled' + } + } ); + + /* * It is useful to have variables which are scoped locally so only the * DataTables functions can access them and they don't leak into global space. @@ -1338,7 +1099,6 @@ // Defined else where // _selector_run // _selector_opts - // _selector_first // _selector_row_indexes var _ext; // DataTable.ext @@ -1348,16 +1108,17 @@ var _re_dic = {}; var _re_new_lines = /[\r\n\u2028]/g; - var _re_html = /<.*?>/g; + var _re_html = /<([^>]*>)/g; + var _max_str_len = Math.pow(2, 28); // This is not strict ISO8601 - Date.parse() is quite lax, although // implementations differ between browsers. - var _re_date = /^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/; + var _re_date = /^\d{2,4}[./-]\d{1,2}[./-]\d{1,2}([T ]{1}\d{1,2}[:.]\d{2}([.:]\d{2})?)?$/; // Escape regular expression special characters var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' ); - // http://en.wikipedia.org/wiki/Foreign_exchange_market + // https://en.wikipedia.org/wiki/Foreign_exchange_market // - \u20BD - Russian ruble. // - \u20a9 - South Korean Won // - \u20BA - Turkish Lira @@ -1427,12 +1188,17 @@ return _empty( d ) || typeof d === 'string'; }; - + // Is a string a number surrounded by HTML? var _htmlNumeric = function ( d, decimalPoint, formatted ) { if ( _empty( d ) ) { return true; } + // input and select strings mean that this isn't just a number + if (typeof d === 'string' && d.match(/<(input|select)/i)) { + return null; + } + var html = _isHtml( d ); return ! html ? null : @@ -1485,7 +1251,9 @@ } else { for ( ; i<ien ; i++ ) { - out.push( a[ order[i] ][ prop ] ); + if ( a[ order[i] ] ) { + out.push( a[ order[i] ][ prop ] ); + } } } @@ -1528,13 +1296,58 @@ return out; }; + // Replaceable function in api.util + var _stripHtml = function (input) { + // Irrelevant check to workaround CodeQL's false positive on the regex + if (input.length > _max_str_len) { + throw new Error('Exceeded max str len'); + } + + var previous; + + input = input.replace(_re_html, ''); // Complete tags + + // Safety for incomplete script tag - use do / while to ensure that + // we get all instances + do { + previous = input; + input = input.replace(/<script/i, ''); + } while (input !== previous); + + return previous; + }; + + // Replaceable function in api.util + var _escapeHtml = function ( d ) { + if (Array.isArray(d)) { + d = d.join(','); + } - var _stripHtml = function ( d ) { - return d - .replace( _re_html, '' ) // Complete tags - .replace(/<script/i, ''); // Safety for incomplete script tag + return typeof d === 'string' ? + d + .replace(/&/g, '&amp;') + .replace(/</g, '&lt;') + .replace(/>/g, '&gt;') + .replace(/"/g, '&quot;') : + d; }; + // Remove diacritics from a string by decomposing it and then removing + // non-ascii characters + var _normalize = function (str, both) { + if (typeof str !== 'string') { + return str; + } + + // It is faster to just run `normalize` than it is to check if + // we need to with a regex! + var res = str.normalize("NFD"); + + // Equally, here we check if a regex is needed or not + return res.length !== str.length + ? (both === true ? str + ' ' : '' ) + res.replace(/[\u0300-\u036f]/g, "") + : res; + } /** * Determine if all values in the array are unique. This means we can short @@ -1574,13 +1387,17 @@ */ var _unique = function ( src ) { + if (Array.from && Set) { + return Array.from(new Set(src)); + } + if ( _areAllUnique( src ) ) { return src.slice(); } // A faster unique method is to use object keys to identify used values, // but this doesn't work with arrays or objects, which we must also - // consider. See jsperf.com/compare-array-unique-versions/4 for more + // consider. See jsperf.app/compare-array-unique-versions/4 for more // information. var out = [], @@ -1615,40 +1432,20 @@ else { out.push(val); } - + return out; } - var _includes = function (search, start) { - if (start === undefined) { - start = 0; + // Similar to jQuery's addClass, but use classList.add + function _addClass(el, name) { + if (name) { + name.split(' ').forEach(function (n) { + if (n) { + // `add` does deduplication, so no need to check `contains` + el.classList.add(n); + } + }); } - - return this.indexOf(search, start) !== -1; - }; - - // Array.isArray polyfill. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray - if (! Array.isArray) { - Array.isArray = function(arg) { - return Object.prototype.toString.call(arg) === '[object Array]'; - }; - } - - if (! Array.prototype.includes) { - Array.prototype.includes = _includes; - } - - // .trim() polyfill - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim - if (!String.prototype.trim) { - String.prototype.trim = function () { - return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); - }; - } - - if (! String.prototype.includes) { - String.prototype.includes = _includes; } /** @@ -1663,6 +1460,43 @@ */ DataTable.util = { /** + * Return a string with diacritic characters decomposed + * @param {*} mixed Function or string to normalize + * @param {*} both Return original string and the normalized string + * @returns String or undefined + */ + diacritics: function (mixed, both) { + var type = typeof mixed; + + if (type !== 'function') { + return _normalize(mixed, both); + } + _normalize = mixed; + }, + + /** + * Debounce a function + * + * @param {function} fn Function to be called + * @param {integer} freq Call frequency in mS + * @return {function} Wrapped function + */ + debounce: function ( fn, timeout ) { + var timer; + + return function () { + var that = this; + var args = arguments; + + clearTimeout(timer); + + timer = setTimeout( function () { + fn.apply(that, args); + }, timeout || 250 ); + }; + }, + + /** * Throttle the calls to a function. Arguments and context are maintained * for the throttled function. * @@ -1697,7 +1531,6 @@ }; }, - /** * Escape a string such that it can be used in a regular expression * @@ -1731,9 +1564,10 @@ source( data, 'set', val, meta ); }; } - else if ( typeof source === 'string' && (source.indexOf('.') !== -1 || - source.indexOf('[') !== -1 || source.indexOf('(') !== -1) ) - { + else if ( + typeof source === 'string' && (source.indexOf('.') !== -1 || + source.indexOf('[') !== -1 || source.indexOf('(') !== -1) + ) { // Like the get, we need to get data from a nested object var setData = function (data, val, src) { var a = _fnSplitObjNotation( src ), b; @@ -1849,9 +1683,10 @@ return source( data, type, row, meta ); }; } - else if ( typeof source === 'string' && (source.indexOf('.') !== -1 || - source.indexOf('[') !== -1 || source.indexOf('(') !== -1) ) - { + else if ( + typeof source === 'string' && (source.indexOf('.') !== -1 || + source.indexOf('[') !== -1 || source.indexOf('(') !== -1) + ) { /* If there is a . in the source string then the data source is in a * nested object so we loop over the data for each level to get the next * level down. On each loop we test for undefined, and if found immediately @@ -1926,11 +1761,39 @@ } else { // Array or flat object mapping - return function (data, type) { // row and meta also passed, but not used + return function (data) { // row and meta also passed, but not used return data[source]; }; } - } + }, + + stripHtml: function (mixed) { + var type = typeof mixed; + + if (type === 'function') { + _stripHtml = mixed; + return; + } + else if (type === 'string') { + return _stripHtml(mixed); + } + return mixed; + }, + + escapeHtml: function (mixed) { + var type = typeof mixed; + + if (type === 'function') { + _escapeHtml = mixed; + return; + } + else if (type === 'string' || Array.isArray(mixed)) { + return _escapeHtml(mixed); + } + return mixed; + }, + + unique: _unique }; @@ -1950,7 +1813,7 @@ newKey, map = {}; - $.each( o, function (key, val) { + $.each( o, function (key) { match = key.match(/^([^A-Z]+?)([A-Z])/); if ( match && hungarian.indexOf(match[1]+' ') !== -1 ) @@ -1988,7 +1851,7 @@ var hungarianKey; - $.each( user, function (key, val) { + $.each( user, function (key) { hungarianKey = src._hungarianMap[ key ]; if ( hungarianKey !== undefined && (force || user[hungarianKey] === undefined) ) @@ -2011,57 +1874,6 @@ } ); } - - /** - * Language compatibility - when certain options are given, and others aren't, we - * need to duplicate the values over, in order to provide backwards compatibility - * with older language files. - * @param {object} oSettings dataTables settings object - * @memberof DataTable#oApi - */ - function _fnLanguageCompat( lang ) - { - // Note the use of the Hungarian notation for the parameters in this method as - // this is called after the mapping of camelCase to Hungarian - var defaults = DataTable.defaults.oLanguage; - - // Default mapping - var defaultDecimal = defaults.sDecimal; - if ( defaultDecimal ) { - _addNumericSort( defaultDecimal ); - } - - if ( lang ) { - var zeroRecords = lang.sZeroRecords; - - // Backwards compatibility - if there is no sEmptyTable given, then use the same as - // sZeroRecords - assuming that is given. - if ( ! lang.sEmptyTable && zeroRecords && - defaults.sEmptyTable === "No data available in table" ) - { - _fnMap( lang, lang, 'sZeroRecords', 'sEmptyTable' ); - } - - // Likewise with loading records - if ( ! lang.sLoadingRecords && zeroRecords && - defaults.sLoadingRecords === "Loading..." ) - { - _fnMap( lang, lang, 'sZeroRecords', 'sLoadingRecords' ); - } - - // Old parameter name of the thousands separator mapped onto the new - if ( lang.sInfoThousands ) { - lang.sThousands = lang.sInfoThousands; - } - - var decimal = lang.sDecimal; - if ( decimal && defaultDecimal !== decimal ) { - _addNumericSort( decimal ); - } - } - } - - /** * Map one parameter onto another * @param {object} o Object to map @@ -2113,6 +1925,11 @@ } } } + + // Enable search delay if server-side processing is enabled + if (init.serverSide && ! init.searchDelay) { + init.searchDelay = 400; + } } @@ -2156,7 +1973,7 @@ .css( { position: 'fixed', top: 0, - left: $(window).scrollLeft()*-1, // allow for scrolling + left: -1 * window.pageXOffset, // allow for scrolling height: 1, width: 1, overflow: 'hidden' @@ -2183,31 +2000,13 @@ var outer = n.children(); var inner = outer.children(); - // Numbers below, in order, are: - // inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth - // - // IE6 XP: 100 100 100 83 - // IE7 Vista: 100 100 100 83 - // IE 8+ Windows: 83 83 100 83 - // Evergreen Windows: 83 83 100 83 - // Evergreen Mac with scrollbars: 85 85 100 85 - // Evergreen Mac without scrollbars: 100 100 100 100 - // Get scrollbar width browser.barWidth = outer[0].offsetWidth - outer[0].clientWidth; - // IE6/7 will oversize a width 100% element inside a scrolling element, to - // include the width of the scrollbar, while other browsers ensure the inner - // element is contained without forcing scrolling - browser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100; - // In rtl text layout, some browsers (most, but not all) will place the // scrollbar on the left, rather than the right. browser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1; - // IE8- don't provide height and width for getBoundingClientRect - browser.bBounding = n[0].getBoundingClientRect().width ? true : false; - n.remove(); } @@ -2215,58 +2014,22 @@ settings.oScroll.iBarWidth = DataTable.__browser.barWidth; } - - /** - * Array.prototype reduce[Right] method, used for browsers which don't support - * JS 1.6. Done this way to reduce code size, since we iterate either way - * @param {object} settings dataTables settings object - * @memberof DataTable#oApi - */ - function _fnReduce ( that, fn, init, start, end, inc ) - { - var - i = start, - value, - isSet = false; - - if ( init !== undefined ) { - value = init; - isSet = true; - } - - while ( i !== end ) { - if ( ! that.hasOwnProperty(i) ) { - continue; - } - - value = isSet ? - fn( value, that[i], i, that ) : - that[i]; - - isSet = true; - i += inc; - } - - return value; - } - /** * Add a column to the list used for the table with default values * @param {object} oSettings dataTables settings object - * @param {node} nTh The th element for this column * @memberof DataTable#oApi */ - function _fnAddColumn( oSettings, nTh ) + function _fnAddColumn( oSettings ) { // Add column to aoColumns array var oDefaults = DataTable.defaults.column; var iCol = oSettings.aoColumns.length; var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, { - "nTh": nTh ? nTh : document.createElement('th'), - "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '', "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol], "mData": oDefaults.mData ? oDefaults.mData : iCol, - idx: iCol + idx: iCol, + searchFixed: {}, + colEl: $('<col>').attr('data-dt-column', iCol) } ); oSettings.aoColumns.push( oCol ); @@ -2275,9 +2038,6 @@ // with only some of the parameters defined, and also not give a default var searchCols = oSettings.aoPreSearchCols; searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] ); - - // Use the default column options function to initialise classes etc - _fnColumnOptions( oSettings, iCol, $(nTh).data() ); } @@ -2291,21 +2051,6 @@ function _fnColumnOptions( oSettings, iCol, oOptions ) { var oCol = oSettings.aoColumns[ iCol ]; - var oClasses = oSettings.oClasses; - var th = $(oCol.nTh); - - // Try to get width information from the DOM. We can't get it from CSS - // as we'd need to parse the CSS stylesheet. `width` option can override - if ( ! oCol.sWidthOrig ) { - // Width attribute - oCol.sWidthOrig = th.attr('width') || null; - - // Style attribute - var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/); - if ( t ) { - oCol.sWidthOrig = t[1]; - } - } /* User specified column options */ if ( oOptions !== undefined && oOptions !== null ) @@ -2326,16 +2071,13 @@ { oCol._sManualType = oOptions.sType; } - + // `class` is a reserved word in Javascript, so we need to provide // the ability to use a valid name for the camel case input if ( oOptions.className && ! oOptions.sClass ) { oOptions.sClass = oOptions.className; } - if ( oOptions.sClass ) { - th.addClass( oOptions.sClass ); - } var origClass = oCol.sClass; @@ -2355,19 +2097,23 @@ { oCol.aDataSort = [ oOptions.iDataSort ]; } - _fnMap( oCol, oOptions, "aDataSort" ); - - // Fall back to the aria-label attribute on the table header if no ariaTitle is - // provided. - if (! oCol.ariaTitle) { - oCol.ariaTitle = th.attr("aria-label"); - } + _fnMap( oCol, oOptions, "aDataSort" ); } /* Cache the data get and set functions for speed */ var mDataSrc = oCol.mData; var mData = _fnGetObjectDataFn( mDataSrc ); - var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; + + // The `render` option can be given as an array to access the helper rendering methods. + // The first element is the rendering method to use, the rest are the parameters to pass + if ( oCol.mRender && Array.isArray( oCol.mRender ) ) { + var copy = oCol.mRender.slice(); + var name = copy.shift(); + + oCol.mRender = DataTable.render[name].apply(window, copy); + } + + oCol._render = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; var attrTest = function( src ) { return typeof src === 'string' && src.indexOf('@') !== -1; @@ -2380,8 +2126,8 @@ oCol.fnGetData = function (rowData, type, meta) { var innerData = mData( rowData, type, undefined, meta ); - return mRender && type ? - mRender( innerData, type, rowData, meta ) : + return oCol._render && type ? + oCol._render( innerData, type, rowData, meta ) : innerData; }; oCol.fnSetData = function ( rowData, val, meta ) { @@ -2398,31 +2144,6 @@ if ( !oSettings.oFeatures.bSort ) { oCol.bSortable = false; - th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called - } - - /* Check that the class assignment is correct for sorting */ - var bAsc = $.inArray('asc', oCol.asSorting) !== -1; - var bDesc = $.inArray('desc', oCol.asSorting) !== -1; - if ( !oCol.bSortable || (!bAsc && !bDesc) ) - { - oCol.sSortingClass = oClasses.sSortableNone; - oCol.sSortingClassJUI = ""; - } - else if ( bAsc && !bDesc ) - { - oCol.sSortingClass = oClasses.sSortableAsc; - oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed; - } - else if ( !bAsc && bDesc ) - { - oCol.sSortingClass = oClasses.sSortableDesc; - oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed; - } - else - { - oCol.sSortingClass = oClasses.sSortable; - oCol.sSortingClassJUI = oClasses.sSortJUI; } } @@ -2435,27 +2156,33 @@ */ function _fnAdjustColumnSizing ( settings ) { - /* Not interested in doing column width calculation if auto-width is disabled */ - if ( settings.oFeatures.bAutoWidth !== false ) - { - var columns = settings.aoColumns; - - _fnCalculateColumnWidths( settings ); - for ( var i=0 , iLen=columns.length ; i<iLen ; i++ ) - { - columns[i].nTh.style.width = columns[i].sWidth; - } - } + _fnCalculateColumnWidths( settings ); + _fnColumnSizes( settings ); var scroll = settings.oScroll; - if ( scroll.sY !== '' || scroll.sX !== '') - { + if ( scroll.sY !== '' || scroll.sX !== '') { _fnScrollDraw( settings ); } _fnCallbackFire( settings, null, 'column-sizing', [settings] ); } + /** + * Apply column sizes + * + * @param {*} settings DataTables settings object + */ + function _fnColumnSizes ( settings ) + { + var cols = settings.aoColumns; + + for (var i=0 ; i<cols.length ; i++) { + var width = _fnColumnsSumWidth(settings, [i], false, false); + + cols[i].colEl.css('width', width); + } + } + /** * Convert the index of a visible column to the index in the data array (take account @@ -2486,7 +2213,7 @@ function _fnColumnIndexToVisible( oSettings, iMatch ) { var aiVis = _fnGetColumns( oSettings, 'bVisible' ); - var iPos = $.inArray( iMatch, aiVis ); + var iPos = aiVis.indexOf(iMatch); return iPos !== -1 ? iPos : null; } @@ -2498,16 +2225,19 @@ * @returns {int} i the number of visible columns * @memberof DataTable#oApi */ - function _fnVisbleColumns( oSettings ) + function _fnVisbleColumns( settings ) { + var layout = settings.aoHeader; + var columns = settings.aoColumns; var vis = 0; - // No reduce in IE8, use a loop for now - $.each( oSettings.aoColumns, function ( i, col ) { - if ( col.bVisible && $(col.nTh).css('display') !== 'none' ) { - vis++; + if ( layout.length ) { + for ( var i=0, ien=layout[0].length ; i<ien ; i++ ) { + if ( columns[i].bVisible && $(layout[0][i].cell).css('display') !== 'none' ) { + vis++; + } } - } ); + } return vis; } @@ -2525,7 +2255,7 @@ { var a = []; - $.map( oSettings.aoColumns, function(val, i) { + oSettings.aoColumns.map( function(val, i) { if ( val[sParam] ) { a.push( i ); } @@ -2546,7 +2276,7 @@ var data = settings.aoData; var types = DataTable.ext.type.detect; var i, ien, j, jen, k, ken; - var col, cell, detectedType, cache; + var col, detectedType, cache; // For each column, spin over the for ( i=0, ien=columns.length ; i<ien ; i++ ) { @@ -2559,6 +2289,11 @@ else if ( ! col.sType ) { for ( j=0, jen=types.length ; j<jen ; j++ ) { for ( k=0, ken=data.length ; k<ken ; k++ ) { + + if (! data[k]) { + continue; + } + // Use a cache array so we only need to get the type data // from the formatter once (when using multiple detectors) if ( cache[k] === undefined ) { @@ -2572,7 +2307,7 @@ // exception for the last type which is `html`. We need to // scan all rows since it is possible to mix string and HTML // types - if ( ! detectedType && j !== types.length-1 ) { + if ( ! detectedType && j !== types.length-2 ) { break; } @@ -2597,9 +2332,61 @@ col.sType = 'string'; } } + + // Set class names for header / footer for auto type classes + var autoClass = _ext.type.className[col.sType]; + + if (autoClass) { + _columnAutoClass(settings.aoHeader, i, autoClass); + _columnAutoClass(settings.aoFooter, i, autoClass); + } + + var renderer = _ext.type.render[col.sType]; + + // This can only happen once! There is no way to remover + // a renderer. After the first time the renderer has + // already been set so createTr will run the renderer itself. + if (renderer && ! col._render) { + col._render = DataTable.util.get(renderer); + + _columnAutoRender(settings, i); + } + } + } + + /** + * Apply an auto detected renderer to data which doesn't yet have + * a renderer + */ + function _columnAutoRender(settings, colIdx) { + var data = settings.aoData; + + for (var i=0 ; i<data.length ; i++) { + if (data[i].nTr) { + // We have to update the display here since there is no + // invalidation check for the data + var display = _fnGetCellData( settings, i, colIdx, 'display' ); + + data[i].displayData[colIdx] = display; + _fnWriteCell(data[i].anCells[colIdx], display); + + // No need to update sort / filter data since it has + // been invalidated and will be re-read with the + // renderer now applied + } } } + /** + * Apply a class name to a column's header cells + */ + function _columnAutoClass(container, colIdx, className) { + container.forEach(function (row) { + if (row[colIdx] && row[colIdx].unique) { + _addClass(row[colIdx].cell, className); + } + }); + } /** * Take the column definitions and static columns arrays and calculate how @@ -2608,15 +2395,24 @@ * @param {object} oSettings dataTables settings object * @param {array} aoColDefs The aoColumnDefs array that is to be applied * @param {array} aoCols The aoColumns array that defines columns individually + * @param {array} headerLayout Layout for header as it was loaded * @param {function} fn Callback function - takes two parameters, the calculated * column index and the definition for that column. * @memberof DataTable#oApi */ - function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, fn ) + function _fnApplyColumnDefs( oSettings, aoColDefs, aoCols, headerLayout, fn ) { var i, iLen, j, jLen, k, kLen, def; var columns = oSettings.aoColumns; + if ( aoCols ) { + for ( i=0, iLen=aoCols.length ; i<iLen ; i++ ) { + if (aoCols[i] && aoCols[i].name) { + columns[i].sName = aoCols[i].name; + } + } + } + // Column definitions with aTargets if ( aoColDefs ) { @@ -2639,32 +2435,56 @@ for ( j=0, jLen=aTargets.length ; j<jLen ; j++ ) { - if ( typeof aTargets[j] === 'number' && aTargets[j] >= 0 ) + var target = aTargets[j]; + + if ( typeof target === 'number' && target >= 0 ) { /* Add columns that we don't yet know about */ - while( columns.length <= aTargets[j] ) + while( columns.length <= target ) { _fnAddColumn( oSettings ); } /* Integer, basic index */ - fn( aTargets[j], def ); + fn( target, def ); } - else if ( typeof aTargets[j] === 'number' && aTargets[j] < 0 ) + else if ( typeof target === 'number' && target < 0 ) { /* Negative integer, right to left column counting */ - fn( columns.length+aTargets[j], def ); + fn( columns.length+target, def ); } - else if ( typeof aTargets[j] === 'string' ) + else if ( typeof target === 'string' ) { - /* Class name matching on TH element */ - for ( k=0, kLen=columns.length ; k<kLen ; k++ ) - { - if ( aTargets[j] == "_all" || - $(columns[k].nTh).hasClass( aTargets[j] ) ) - { + for ( k=0, kLen=columns.length ; k<kLen ; k++ ) { + if (target === '_all') { + // Apply to all columns fn( k, def ); } + else if (target.indexOf(':name') !== -1) { + // Column selector + if (columns[k].sName === target.replace(':name', '')) { + fn( k, def ); + } + } + else { + // Cell selector + headerLayout.forEach(function (row) { + if (row[k]) { + var cell = $(row[k].cell); + + // Legacy support. Note that it means that we don't support + // an element name selector only, since they are treated as + // class names for 1.x compat. + if (target.match(/^[a-z][\w-]*$/i)) { + target = '.' + target; + } + + if (cell.is( target )) { + fn( k, def ); + } + } + }); + } } } } @@ -2672,65 +2492,124 @@ } // Statically defined columns array - if ( aoCols ) - { - for ( i=0, iLen=aoCols.length ; i<iLen ; i++ ) - { + if ( aoCols ) { + for ( i=0, iLen=aoCols.length ; i<iLen ; i++ ) { fn( i, aoCols[i] ); } } } + + /** + * Get the width for a given set of columns + * + * @param {*} settings DataTables settings object + * @param {*} targets Columns - comma separated string or array of numbers + * @param {*} original Use the original width (true) or calculated (false) + * @param {*} incVisible Include visible columns (true) or not (false) + * @returns Combined CSS value + */ + function _fnColumnsSumWidth( settings, targets, original, incVisible ) { + if ( ! Array.isArray( targets ) ) { + targets = _fnColumnsFromHeader( targets ); + } + + var sum = 0; + var unit; + var columns = settings.aoColumns; + + for ( var i=0, ien=targets.length ; i<ien ; i++ ) { + var column = columns[ targets[i] ]; + var definedWidth = original ? + column.sWidthOrig : + column.sWidth; + + if ( ! incVisible && column.bVisible === false ) { + continue; + } + + if ( definedWidth === null || definedWidth === undefined ) { + return null; // can't determine a defined width - browser defined + } + else if ( typeof definedWidth === 'number' ) { + unit = 'px'; + sum += definedWidth; + } + else { + var matched = definedWidth.match(/([\d\.]+)([^\d]*)/); + + if ( matched ) { + sum += matched[1] * 1; + unit = matched.length === 3 ? + matched[2] : + 'px'; + } + } + } + + return sum + unit; + } + + function _fnColumnsFromHeader( cell ) + { + var attr = $(cell).closest('[data-dt-column]').attr('data-dt-column'); + + if ( ! attr ) { + return []; + } + + return attr.split(',').map( function (val) { + return val * 1; + } ); + } /** * Add a data array to the table, creating DOM node etc. This is the parallel to * _fnGatherData, but for adding rows from a Javascript source, rather than a * DOM source. - * @param {object} oSettings dataTables settings object - * @param {array} aData data array to be added - * @param {node} [nTr] TR element to add to the table - optional. If not given, + * @param {object} settings dataTables settings object + * @param {array} data data array to be added + * @param {node} [tr] TR element to add to the table - optional. If not given, * DataTables will create a row automatically - * @param {array} [anTds] Array of TD|TH elements for the row - must be given + * @param {array} [tds] Array of TD|TH elements for the row - must be given * if nTr is. * @returns {int} >=0 if successful (index of new aoData entry), -1 if failed * @memberof DataTable#oApi */ - function _fnAddData ( oSettings, aDataIn, nTr, anTds ) + function _fnAddData ( settings, dataIn, tr, tds ) { /* Create the object for storing information about this new row */ - var iRow = oSettings.aoData.length; - var oData = $.extend( true, {}, DataTable.models.oRow, { - src: nTr ? 'dom' : 'data', - idx: iRow + var rowIdx = settings.aoData.length; + var rowModel = $.extend( true, {}, DataTable.models.oRow, { + src: tr ? 'dom' : 'data', + idx: rowIdx } ); - oData._aData = aDataIn; - oSettings.aoData.push( oData ); + rowModel._aData = dataIn; + settings.aoData.push( rowModel ); - /* Create the cells */ - var nTd, sThisType; - var columns = oSettings.aoColumns; + var columns = settings.aoColumns; - // Invalidate the column types as the new data needs to be revalidated for ( var i=0, iLen=columns.length ; i<iLen ; i++ ) { + // Invalidate the column types as the new data needs to be revalidated columns[i].sType = null; } /* Add to the display array */ - oSettings.aiDisplayMaster.push( iRow ); + settings.aiDisplayMaster.push( rowIdx ); - var id = oSettings.rowIdFn( aDataIn ); + var id = settings.rowIdFn( dataIn ); if ( id !== undefined ) { - oSettings.aIds[ id ] = oData; + settings.aIds[ id ] = rowModel; } /* Create the DOM information, or register it if already present */ - if ( nTr || ! oSettings.oFeatures.bDeferRender ) + if ( tr || ! settings.oFeatures.bDeferRender ) { - _fnCreateTr( oSettings, iRow, nTr, anTds ); + _fnCreateTr( settings, rowIdx, tr, tds ); } - return iRow; + return rowIdx; } @@ -2761,33 +2640,6 @@ /** - * Take a TR element and convert it to an index in aoData - * @param {object} oSettings dataTables settings object - * @param {node} n the TR element to find - * @returns {int} index if the node is found, null if not - * @memberof DataTable#oApi - */ - function _fnNodeToDataIndex( oSettings, n ) - { - return (n._DT_RowIndex!==undefined) ? n._DT_RowIndex : null; - } - - - /** - * Take a TD element and convert it into a column data index (not the visible index) - * @param {object} oSettings dataTables settings object - * @param {int} iRow The row number the TD/TH can be found in - * @param {node} n The TD/TH element to find - * @returns {int} index if the node is found, -1 if not - * @memberof DataTable#oApi - */ - function _fnNodeToColumnIndex( oSettings, iRow, n ) - { - return $.inArray( n, oSettings.aoData[ iRow ].anCells ); - } - - - /** * Get the data for a given cell from the internal cache, taking into account data mapping * @param {object} settings dataTables settings object * @param {int} rowIdx aoData row id @@ -2805,9 +2657,15 @@ type = 'sort'; } + var row = settings.aoData[rowIdx]; + + if (! row) { + return undefined; + } + var draw = settings.iDraw; var col = settings.aoColumns[colIdx]; - var rowData = settings.aoData[rowIdx]._aData; + var rowData = row._aData; var defaultContent = col.sDefaultContent; var cellData = col.fnGetData( rowData, type, { settings: settings, @@ -2815,6 +2673,11 @@ col: colIdx } ); + // Allow for a node being returned for non-display types + if (type !== 'display' && cellData && typeof cellData === 'object' && cellData.nodeName) { + cellData = cellData.innerHTML; + } + if ( cellData === undefined ) { if ( settings.iDrawError != draw && defaultContent === null ) { _fnLog( settings, 0, "Requested unknown parameter "+ @@ -2872,6 +2735,23 @@ } ); } + /** + * Write a value to a cell + * @param {*} td Cell + * @param {*} val Value + */ + function _fnWriteCell(td, val) + { + if (val && typeof val === 'object' && val.nodeName) { + $(td) + .empty() + .append(val); + } + else { + td.innerHTML = val; + } + } + // Private variable that is used to match action syntax in the data property object var __reArray = /\[.*?\]$/; @@ -2884,7 +2764,9 @@ */ function _fnSplitObjNotation( str ) { - return $.map( str.match(/(\\.|[^\.])+/g) || [''], function ( s ) { + var parts = str.match(/(\\.|[^.])+/g) || ['']; + + return parts.map( function ( s ) { return s.replace(/\\\./g, '.'); } ); } @@ -2936,36 +2818,6 @@ } - /** - * Take an array of integers (index array) and remove a target integer (value - not - * the key!) - * @param {array} a Index array to target - * @param {int} iTarget value to find - * @memberof DataTable#oApi - */ - function _fnDeleteIndex( a, iTarget, splice ) - { - var iTargetIndex = -1; - - for ( var i=0, iLen=a.length ; i<iLen ; i++ ) - { - if ( a[i] == iTarget ) - { - iTargetIndex = i; - } - else if ( a[i] > iTarget ) - { - a[i]--; - } - } - - if ( iTargetIndex != -1 && splice === undefined ) - { - a.splice( iTargetIndex, 1 ); - } - } - - /** * Mark cached data as invalid such that a re-read of the data will occur when * the cached data is next requested. Also update from the data source object. @@ -2986,16 +2838,11 @@ { var row = settings.aoData[ rowIdx ]; var i, ien; - var cellWrite = function ( cell, col ) { - // This is very frustrating, but in IE if you just write directly - // to innerHTML, and elements that are overwritten are GC'ed, - // even if there is a reference to them elsewhere - while ( cell.childNodes.length ) { - cell.removeChild( cell.firstChild ); - } - cell.innerHTML = _fnGetCellData( settings, rowIdx, col, 'display' ); - }; + // Remove the cached data for the row + row._aSortData = null; + row._aFilterData = null; + row.displayData = null; // Are we reading last data from DOM or the data object? if ( src === 'dom' || ((! src || src === 'auto') && row.src === 'dom') ) { @@ -3008,33 +2855,34 @@ else { // Reading from data object, update the DOM var cells = row.anCells; + var display = _fnGetRowDisplay(settings, rowIdx); if ( cells ) { if ( colIdx !== undefined ) { - cellWrite( cells[colIdx], colIdx ); + _fnWriteCell(cells[colIdx], display[colIdx]); } else { for ( i=0, ien=cells.length ; i<ien ; i++ ) { - cellWrite( cells[i], i ); + _fnWriteCell(cells[i], display[i]); } } } } - // For both row and cell invalidation, the cached data for sorting and - // filtering is nulled out - row._aSortData = null; - row._aFilterData = null; - - // Invalidate the type for a specific column (if given) or all columns since - // the data might have changed + // Column specific invalidation var cols = settings.aoColumns; if ( colIdx !== undefined ) { + // Type - the data might have changed cols[ colIdx ].sType = null; + + // Max length string. Its a fairly cheep recalculation, so not worth + // something more complicated + cols[ colIdx ].maxLenString = null; } else { for ( i=0, ien=cols.length ; i<ien ; i++ ) { cols[i].sType = null; + cols[i].maxLenString = null; } // Update DataTables special `DT_*` attributes for the row @@ -3065,7 +2913,7 @@ var tds = [], td = row.firstChild, - name, col, o, i=0, contents, + name, col, i=0, contents, columns = settings.aoColumns, objectRead = settings._rowReadObject; @@ -3159,6 +3007,29 @@ cells: tds }; } + + /** + * Render and cache a row's display data for the columns, if required + * @returns + */ + function _fnGetRowDisplay (settings, rowIdx) { + let rowModal = settings.aoData[rowIdx]; + let columns = settings.aoColumns; + + if (! rowModal.displayData) { + // Need to render and cache + rowModal.displayData = []; + + for ( var colIdx=0, len=columns.length ; colIdx<len ; colIdx++ ) { + rowModal.displayData.push( + _fnGetCellData( settings, rowIdx, colIdx, 'display' ) + ); + } + } + + return rowModal.displayData; + } + /** * Create a new TR element (and it's TD children) for a row * @param {object} oSettings dataTables settings object @@ -3176,7 +3047,8 @@ rowData = row._aData, cells = [], nTr, nTd, oCol, - i, iLen, create; + i, iLen, create, + trClass = oSettings.oClasses.tbody.row; if ( row.nTr === null ) { @@ -3185,6 +3057,8 @@ row.nTr = nTr; row.anCells = cells; + _addClass(nTr, trClass); + /* Use a private property on the node to allow reserve mapping from the node * to the aoData array for fast look up */ @@ -3197,7 +3071,7 @@ for ( i=0, iLen=oSettings.aoColumns.length ; i<iLen ; i++ ) { oCol = oSettings.aoColumns[i]; - create = nTrIn ? false : true; + create = nTrIn && anTds[i] ? false : true; nTd = create ? document.createElement( oCol.sCellType ) : anTds[i]; @@ -3211,26 +3085,26 @@ }; cells.push( nTd ); + + var display = _fnGetRowDisplay(oSettings, iRow); // Need to create the HTML if new, or if a rendering function is defined - if ( create || ((oCol.mRender || oCol.mData !== i) && - (!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display') - )) { - nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' ); - } - - /* Add user defined class */ - if ( oCol.sClass ) - { - nTd.className += ' '+oCol.sClass; + if ( + create || + ( + (oCol.mRender || oCol.mData !== i) && + (!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display') + ) + ) { + _fnWriteCell(nTd, display[i]); } // Visibility - add or remove as required - if ( oCol.bVisible && ! nTrIn ) + if ( oCol.bVisible && create ) { nTr.appendChild( nTd ); } - else if ( ! oCol.bVisible && nTrIn ) + else if ( ! oCol.bVisible && ! create ) { nTd.parentNode.removeChild( nTd ); } @@ -3243,7 +3117,10 @@ } } - _fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow, cells] ); + _fnCallbackFire( oSettings, 'aoRowCreatedCallback', 'row-created', [nTr, rowData, iRow, cells] ); + } + else { + _addClass(row.nTr, trClass); } } @@ -3295,182 +3172,184 @@ * @param {object} oSettings dataTables settings object * @memberof DataTable#oApi */ - function _fnBuildHead( oSettings ) + function _fnBuildHead( settings, side ) { - var i, ien, cell, row, column; - var thead = oSettings.nTHead; - var tfoot = oSettings.nTFoot; - var createHeader = $('th, td', thead).length === 0; - var classes = oSettings.oClasses; - var columns = oSettings.aoColumns; - - if ( createHeader ) { - row = $('<tr/>').appendTo( thead ); + var classes = settings.oClasses; + var columns = settings.aoColumns; + var i, ien, row; + var target = side === 'header' + ? settings.nTHead + : settings.nTFoot; + var titleProp = side === 'header' ? 'sTitle' : side; + + // Footer might be defined + if (! target) { + return; } - for ( i=0, ien=columns.length ; i<ien ; i++ ) { - column = columns[i]; - cell = $( column.nTh ).addClass( column.sClass ); + // If no cells yet and we have content for them, then create + if (side === 'header' || _pluck(settings.aoColumns, titleProp).join('')) { + row = $('tr', target); - if ( createHeader ) { - cell.appendTo( row ); + // Add a row if needed + if (! row.length) { + row = $('<tr/>').appendTo(target) } - // 1.11 move into sorting - if ( oSettings.oFeatures.bSort ) { - cell.addClass( column.sSortingClass ); - - if ( column.bSortable !== false ) { - cell - .attr( 'tabindex', oSettings.iTabIndex ) - .attr( 'aria-controls', oSettings.sTableId ); + // Add the number of cells needed to make up to the number of columns + if (row.length === 1) { + var cells = $('td, th', row); - _fnSortAttachListener( oSettings, column.nTh, i ); + for ( i=cells.length, ien=columns.length ; i<ien ; i++ ) { + $('<th/>') + .html( columns[i][titleProp] || '' ) + .appendTo( row ); } } + } - if ( column.sTitle != cell[0].innerHTML ) { - cell.html( column.sTitle ); - } + var detected = _fnDetectHeader( settings, target, true ); - _fnRenderer( oSettings, 'header' )( - oSettings, cell, column, classes - ); + if (side === 'header') { + settings.aoHeader = detected; } - - if ( createHeader ) { - _fnDetectHeader( oSettings.aoHeader, thead ); + else { + settings.aoFooter = detected; } - /* Deal with the footer - add classes if required */ - $(thead).children('tr').children('th, td').addClass( classes.sHeaderTH ); - $(tfoot).children('tr').children('th, td').addClass( classes.sFooterTH ); + // ARIA role for the rows + $(target).children('tr').attr('role', 'row'); - // Cache the footer cells. Note that we only take the cells from the first - // row in the footer. If there is more than one row the user wants to - // interact with, they need to use the table().foot() method. Note also this - // allows cells to be used for multiple columns using colspan - if ( tfoot !== null ) { - var cells = oSettings.aoFooter[0]; - - for ( i=0, ien=cells.length ; i<ien ; i++ ) { - column = columns[i]; - - if (column) { - column.nTf = cells[i].cell; - - if ( column.sClass ) { - $(column.nTf).addClass( column.sClass ); - } - } - else { - _fnLog( oSettings, 0, 'Incorrect column count', 18 ); - } - } - } + // Every cell needs to be passed through the renderer + $(target).children('tr').children('th, td') + .each( function () { + _fnRenderer( settings, side )( + settings, $(this), classes + ); + } ); } - /** - * Draw the header (or footer) element based on the column visibility states. The - * methodology here is to use the layout array from _fnDetectHeader, modified for - * the instantaneous column visibility, to construct the new layout. The grid is - * traversed over cell at a time in a rows x columns grid fashion, although each - * cell insert can cover multiple elements in the grid - which is tracks using the - * aApplied array. Cell inserts in the grid will only occur where there isn't - * already a cell in that position. - * @param {object} oSettings dataTables settings object - * @param array {objects} aoSource Layout array from _fnDetectHeader - * @param {boolean} [bIncludeHidden=false] If true then include the hidden columns in the calc, - * @memberof DataTable#oApi + * Build a layout structure for a header or footer + * + * @param {*} settings DataTables settings + * @param {*} source Source layout array + * @param {*} incColumns What columns should be included + * @returns Layout array */ - function _fnDrawHead( oSettings, aoSource, bIncludeHidden ) + function _fnHeaderLayout( settings, source, incColumns ) { - var i, iLen, j, jLen, k, kLen, n, nLocalTr; - var aoLocal = []; - var aApplied = []; - var iColumns = oSettings.aoColumns.length; - var iRowspan, iColspan; + var row, column, cell; + var local = []; + var structure = []; + var columns = settings.aoColumns; + var columnCount = columns.length; + var rowspan, colspan; - if ( ! aoSource ) - { + if ( ! source ) { return; } - if ( bIncludeHidden === undefined ) - { - bIncludeHidden = false; + // Default is to work on only visible columns + if ( ! incColumns ) { + incColumns = _range(columnCount) + .filter(function (idx) { + return columns[idx].bVisible; + }); } - /* Make a copy of the master layout array, but without the visible columns in it */ - for ( i=0, iLen=aoSource.length ; i<iLen ; i++ ) - { - aoLocal[i] = aoSource[i].slice(); - aoLocal[i].nTr = aoSource[i].nTr; - - /* Remove any columns which are currently hidden */ - for ( j=iColumns-1 ; j>=0 ; j-- ) - { - if ( !oSettings.aoColumns[j].bVisible && !bIncludeHidden ) - { - aoLocal[i].splice( j, 1 ); - } - } + // Make a copy of the master layout array, but with only the columns we want + for ( row=0 ; row<source.length ; row++ ) { + // Remove any columns we haven't selected + local[row] = source[row].slice().filter(function (cell, i) { + return incColumns.includes(i); + }); - /* Prep the applied array - it needs an element for each row */ - aApplied.push( [] ); + // Prep the structure array - it needs an element for each row + structure.push( [] ); } - for ( i=0, iLen=aoLocal.length ; i<iLen ; i++ ) - { - nLocalTr = aoLocal[i].nTr; + for ( row=0 ; row<local.length ; row++ ) { + for ( column=0 ; column<local[row].length ; column++ ) { + rowspan = 1; + colspan = 1; - /* All cells are going to be replaced, so empty out the row */ - if ( nLocalTr ) - { - while( (n = nLocalTr.firstChild) ) - { - nLocalTr.removeChild( n ); + // Check to see if there is already a cell (row/colspan) covering our target + // insert point. If there is, then there is nothing to do. + if ( structure[row][column] === undefined ) { + cell = local[row][column].cell; + + // Expand for rowspan + while ( + local[row+rowspan] !== undefined && + local[row][column].cell == local[row+rowspan][column].cell + ) { + structure[row+rowspan][column] = null; + rowspan++; + } + + // And for colspan + while ( + local[row][column+colspan] !== undefined && + local[row][column].cell == local[row][column+colspan].cell + ) { + // Which also needs to go over rows + for ( var k=0 ; k<rowspan ; k++ ) { + structure[row+k][column+colspan] = null; + } + + colspan++; + } + + var titleSpan = $('span.dt-column-title', cell); + + structure[row][column] = { + cell: cell, + colspan: colspan, + rowspan: rowspan, + title: titleSpan.length + ? titleSpan.html() + : $(cell).html() + }; } } + } - for ( j=0, jLen=aoLocal[i].length ; j<jLen ; j++ ) - { - iRowspan = 1; - iColspan = 1; + return structure; + } - /* Check to see if there is already a cell (row/colspan) covering our target - * insert point. If there is, then there is nothing to do. - */ - if ( aApplied[i][j] === undefined ) - { - nLocalTr.appendChild( aoLocal[i][j].cell ); - aApplied[i][j] = 1; - /* Expand the cell to cover as many rows as needed */ - while ( aoLocal[i+iRowspan] !== undefined && - aoLocal[i][j].cell == aoLocal[i+iRowspan][j].cell ) - { - aApplied[i+iRowspan][j] = 1; - iRowspan++; - } + /** + * Draw the header (or footer) element based on the column visibility states. + * + * @param object oSettings dataTables settings object + * @param array aoSource Layout array from _fnDetectHeader + * @memberof DataTable#oApi + */ + function _fnDrawHead( settings, source ) + { + var layout = _fnHeaderLayout(settings, source); + var tr, n; - /* Expand the cell to cover as many columns as needed */ - while ( aoLocal[i][j+iColspan] !== undefined && - aoLocal[i][j].cell == aoLocal[i][j+iColspan].cell ) - { - /* Must update the applied array over the rows for the columns */ - for ( k=0 ; k<iRowspan ; k++ ) - { - aApplied[i+k][j+iColspan] = 1; - } - iColspan++; - } + for ( var row=0 ; row<source.length ; row++ ) { + tr = source[row].row; + + // All cells are going to be replaced, so empty out the row + // Can't use $().empty() as that kills event handlers + if (tr) { + while( (n = tr.firstChild) ) { + tr.removeChild( n ); + } + } - /* Do the actual expansion in the DOM */ - $(aoLocal[i][j].cell) - .attr('rowspan', iRowspan) - .attr('colspan', iColspan); + for ( var column=0 ; column<layout[row].length ; column++ ) { + var point = layout[row][column]; + + if (point) { + $(point.cell) + .appendTo(tr) + .attr('rowspan', point.rowspan) + .attr('colspan', point.colspan); } } } @@ -3490,7 +3369,7 @@ /* Provide a pre-callback function which can be used to cancel the draw is false is returned */ var aPreDraw = _fnCallbackFire( oSettings, 'aoPreDrawCallback', 'preDraw', [oSettings] ); - if ( $.inArray( false, aPreDraw ) !== -1 ) + if ( aPreDraw.indexOf(false) !== -1 ) { _fnProcessingDisplay( oSettings, false ); return; @@ -3498,29 +3377,27 @@ var anRows = []; var iRowCount = 0; - var asStripeClasses = oSettings.asStripeClasses; - var iStripes = asStripeClasses.length; - var oLang = oSettings.oLanguage; var bServerSide = _fnDataSource( oSettings ) == 'ssp'; var aiDisplay = oSettings.aiDisplay; var iDisplayStart = oSettings._iDisplayStart; var iDisplayEnd = oSettings.fnDisplayEnd(); + var columns = oSettings.aoColumns; + var body = $(oSettings.nTBody); oSettings.bDrawing = true; /* Server-side processing draw intercept */ - if ( oSettings.bDeferLoading ) - { - oSettings.bDeferLoading = false; - oSettings.iDraw++; - _fnProcessingDisplay( oSettings, false ); - } - else if ( !bServerSide ) + if ( !bServerSide ) { oSettings.iDraw++; } else if ( !oSettings.bDestroying && !ajaxComplete) { + // Show loading message for server-side processing + if (oSettings.iDraw === 0) { + body.empty().append(_emptyRow(oSettings)); + } + _fnAjaxUpdate( oSettings ); return; } @@ -3541,15 +3418,14 @@ var nRow = aoData.nTr; - /* Remove the old striping classes and then add the new one */ - if ( iStripes !== 0 ) - { - var sStripe = asStripeClasses[ iRowCount % iStripes ]; - if ( aoData._sRowStripe != sStripe ) - { - $(nRow).removeClass( aoData._sRowStripe ).addClass( sStripe ); - aoData._sRowStripe = sStripe; - } + // Add various classes as needed + for (var i=0 ; i<columns.length ; i++) { + var col = columns[i]; + var td = aoData.anCells[i]; + + _addClass(td, _ext.type.className[col.sType]); // auto class + _addClass(td, col.sClass); // column class + _addClass(td, oSettings.oClasses.tbody.cell); // all cells } // Row callback functions - might want to manipulate the row @@ -3564,23 +3440,7 @@ } else { - /* Table is empty - create a row with an empty message in it */ - var sZero = oLang.sZeroRecords; - if ( oSettings.iDraw == 1 && _fnDataSource( oSettings ) == 'ajax' ) - { - sZero = oLang.sLoadingRecords; - } - else if ( oLang.sEmptyTable && oSettings.fnRecordsTotal() === 0 ) - { - sZero = oLang.sEmptyTable; - } - - anRows[ 0 ] = $( '<tr/>', { 'class': iStripes ? asStripeClasses[0] : '' } ) - .append( $('<td />', { - 'valign': 'top', - 'colSpan': _fnVisbleColumns( oSettings ), - 'class': oSettings.oClasses.sRowEmpty - } ).html( sZero ) )[0]; + anRows[ 0 ] = _emptyRow(oSettings); } /* Header and footer callbacks */ @@ -3590,13 +3450,21 @@ _fnCallbackFire( oSettings, 'aoFooterCallback', 'footer', [ $(oSettings.nTFoot).children('tr')[0], _fnGetDataMaster( oSettings ), iDisplayStart, iDisplayEnd, aiDisplay ] ); - var body = $(oSettings.nTBody); + // replaceChildren is faster, but only became widespread in 2020, + // so a fall back in jQuery is provided for older browsers. + if (body[0].replaceChildren) { + body[0].replaceChildren.apply(body[0], anRows); + } + else { + body.children().detach(); + body.append( $(anRows) ); + } - body.children().detach(); - body.append( $(anRows) ); + // Empty table needs a specific class + $(oSettings.nTableWrapper).toggleClass('dt-empty-footer', $('tr', oSettings.nTFoot).length === 0); /* Call all required callback functions for the end of a draw */ - _fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings] ); + _fnCallbackFire( oSettings, 'aoDrawCallback', 'draw', [oSettings], true ); /* Draw is complete, sorting and filtering must be as well */ oSettings.bSorted = false; @@ -3612,23 +3480,25 @@ * the paging is reset to the first page * @memberof DataTable#oApi */ - function _fnReDraw( settings, holdPosition ) + function _fnReDraw( settings, holdPosition, recompute ) { var features = settings.oFeatures, sort = features.bSort, filter = features.bFilter; - if ( sort ) { - _fnSort( settings ); - } + if (recompute === undefined || recompute === true) { + if ( sort ) { + _fnSort( settings ); + } - if ( filter ) { - _fnFilterComplete( settings, settings.oPreviousSearch ); - } - else { - // No filtering, so we want to just use the display master - settings.aiDisplay = settings.aiDisplayMaster.slice(); + if ( filter ) { + _fnFilterComplete( settings, settings.oPreviousSearch ); + } + else { + // No filtering, so we want to just use the display master + settings.aiDisplay = settings.aiDisplayMaster.slice(); + } } if ( holdPosition !== true ) { @@ -3645,155 +3515,324 @@ } + /* + * Table is empty - create a row with an empty message in it + */ + function _emptyRow ( settings ) { + var oLang = settings.oLanguage; + var zero = oLang.sZeroRecords; + var dataSrc = _fnDataSource( settings ); + + if ( + (settings.iDraw < 1 && dataSrc === 'ssp') || + (settings.iDraw <= 1 && dataSrc === 'ajax') + ) { + zero = oLang.sLoadingRecords; + } + else if ( oLang.sEmptyTable && settings.fnRecordsTotal() === 0 ) + { + zero = oLang.sEmptyTable; + } + + return $( '<tr/>' ) + .append( $('<td />', { + 'colSpan': _fnVisbleColumns( settings ), + 'class': settings.oClasses.empty.row + } ).html( zero ) )[0]; + } + + + /** + * Convert a `layout` object given by a user to the object structure needed + * for the renderer. This is done twice, once for above and once for below + * the table. Ordering must also be considered. + * + * @param {*} settings DataTables settings object + * @param {*} layout Layout object to convert + * @param {string} side `top` or `bottom` + * @returns Converted array structure - one item for each row. + */ + function _layoutArray ( settings, layout, side ) + { + var groups = {}; + + // Combine into like groups (e.g. `top`, `top2`, etc) + $.each( layout, function ( pos, val ) { + if (val === null) { + return; + } + + var splitPos = pos.replace(/([A-Z])/g, ' $1').split(' '); + + if ( ! groups[ splitPos[0] ] ) { + groups[ splitPos[0] ] = {}; + } + + var align = splitPos.length === 1 ? + 'full' : + splitPos[1].toLowerCase(); + var group = groups[ splitPos[0] ]; + var groupRun = function (contents, innerVal) { + // If it is an object, then there can be multiple features contained in it + if ( $.isPlainObject( innerVal ) ) { + Object.keys(innerVal).map(function (key) { + contents.push( { + feature: key, + opts: innerVal[key] + }); + }); + } + else { + contents.push(innerVal); + } + } + + // Transform to an object with a contents property + if (! group[align] || ! group[align].contents) { + group[align] = { contents: [] }; + } + + // Allow for an array or just a single object + if ( Array.isArray(val)) { + for (var i=0 ; i<val.length ; i++) { + groupRun(group[align].contents, val[i]); + } + } + else { + groupRun(group[ align ].contents, val); + } + + // And make contents an array + if ( ! Array.isArray( group[ align ].contents ) ) { + group[ align ].contents = [ group[ align ].contents ]; + } + } ); + + var filtered = Object.keys(groups) + .map( function ( pos ) { + // Filter to only the side we need + if ( pos.indexOf(side) !== 0 ) { + return null; + } + + return { + name: pos, + val: groups[pos] + }; + } ) + .filter( function (item) { + return item !== null; + }); + + // Order by item identifier + filtered.sort( function ( a, b ) { + var order1 = a.name.replace(/[^0-9]/g, '') * 1; + var order2 = b.name.replace(/[^0-9]/g, '') * 1; + + return order2 - order1; + } ); + + if ( side === 'bottom' ) { + filtered.reverse(); + } + + // Split into rows + var rows = []; + for ( var i=0, ien=filtered.length ; i<ien ; i++ ) { + if ( filtered[i].val.full ) { + rows.push( { full: filtered[i].val.full } ); + _layoutResolve( settings, rows[ rows.length - 1 ] ); + + delete filtered[i].val.full; + } + + if ( Object.keys(filtered[i].val).length ) { + rows.push( filtered[i].val ); + _layoutResolve( settings, rows[ rows.length - 1 ] ); + } + } + + return rows; + } + + + /** + * Convert the contents of a row's layout object to nodes that can be inserted + * into the document by a renderer. Execute functions, look up plug-ins, etc. + * + * @param {*} settings DataTables settings object + * @param {*} row Layout object for this row + */ + function _layoutResolve( settings, row ) { + var getFeature = function (feature, opts) { + if ( ! _ext.features[ feature ] ) { + _fnLog( settings, 0, 'Unknown feature: '+ feature ); + } + + return _ext.features[ feature ].apply( this, [settings, opts] ); + }; + + var resolve = function ( item ) { + var line = row[ item ].contents; + + for ( var i=0, ien=line.length ; i<ien ; i++ ) { + if ( ! line[i] ) { + continue; + } + else if ( typeof line[i] === 'string' ) { + line[i] = getFeature( line[i], null ); + } + else if ( $.isPlainObject(line[i]) ) { + // If it's an object, it just has feature and opts properties from + // the transform in _layoutArray + line[i] = getFeature(line[i].feature, line[i].opts); + } + else if ( typeof line[i].node === 'function' ) { + line[i] = line[i].node( settings ); + } + else if ( typeof line[i] === 'function' ) { + var inst = line[i]( settings ); + + line[i] = typeof inst.node === 'function' ? + inst.node() : + inst; + } + } + }; + + $.each( row, function ( key ) { + resolve( key ); + } ); + } + + /** * Add the options to the page HTML for the table - * @param {object} oSettings dataTables settings object + * @param {object} settings DataTables settings object * @memberof DataTable#oApi */ - function _fnAddOptionsHtml ( oSettings ) + function _fnAddOptionsHtml ( settings ) { - var classes = oSettings.oClasses; - var table = $(oSettings.nTable); - var holding = $('<div/>').insertBefore( table ); // Holding element for speed - var features = oSettings.oFeatures; - - // All DataTables are wrapped in a div - var insert = $('<div/>', { - id: oSettings.sTableId+'_wrapper', - 'class': classes.sWrapper + (oSettings.nTFoot ? '' : ' '+classes.sNoFooter) - } ); + var classes = settings.oClasses; + var table = $(settings.nTable); - oSettings.nHolding = holding[0]; - oSettings.nTableWrapper = insert[0]; - oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling; + // Wrapper div around everything DataTables controls + var insert = $('<div/>') + .attr({ + id: settings.sTableId+'_wrapper', + 'class': classes.container + }) + .insertBefore(table); - /* Loop over the user set positioning and place the elements as needed */ - var aDom = oSettings.sDom.split(''); - var featureNode, cOption, nNewNode, cNext, sAttr, j; - for ( var i=0 ; i<aDom.length ; i++ ) - { + settings.nTableWrapper = insert[0]; + + if (settings.sDom) { + // Legacy + _fnLayoutDom(settings, settings.sDom, insert); + } + else { + var top = _layoutArray( settings, settings.layout, 'top' ); + var bottom = _layoutArray( settings, settings.layout, 'bottom' ); + var renderer = _fnRenderer( settings, 'layout' ); + + // Everything above - the renderer will actually insert the contents into the document + top.forEach(function (item) { + renderer( settings, insert, item ); + }); + + // The table - always the center of attention + renderer( settings, insert, { + full: { + table: true, + contents: [ _fnFeatureHtmlTable(settings) ] + } + } ); + + // Everything below + bottom.forEach(function (item) { + renderer( settings, insert, item ); + }); + } + + // Processing floats on top, so it isn't an inserted feature + _processingHtml( settings ); + } + + /** + * Draw the table with the legacy DOM property + * @param {*} settings DT settings object + * @param {*} dom DOM string + * @param {*} insert Insert point + */ + function _fnLayoutDom( settings, dom, insert ) + { + var parts = dom.match(/(".*?")|('.*?')|./g); + var featureNode, option, newNode, next, attr; + + for ( var i=0 ; i<parts.length ; i++ ) { featureNode = null; - cOption = aDom[i]; + option = parts[i]; - if ( cOption == '<' ) - { - /* New container div */ - nNewNode = $('<div/>')[0]; + if ( option == '<' ) { + // New container div + newNode = $('<div/>'); - /* Check to see if we should append an id and/or a class name to the container */ - cNext = aDom[i+1]; - if ( cNext == "'" || cNext == '"' ) - { - sAttr = ""; - j = 2; - while ( aDom[i+j] != cNext ) - { - sAttr += aDom[i+j]; - j++; - } + // Check to see if we should append an id and/or a class name to the container + next = parts[i+1]; - /* Replace jQuery UI constants @todo depreciated */ - if ( sAttr == "H" ) - { - sAttr = classes.sJUIHeader; - } - else if ( sAttr == "F" ) - { - sAttr = classes.sJUIFooter; - } + if ( next[0] == "'" || next[0] == '"' ) { + attr = next.replace(/['"]/g, ''); + + var id = '', className; /* The attribute can be in the format of "#id.class", "#id" or "class" This logic * breaks the string into parts and applies them as needed */ - if ( sAttr.indexOf('.') != -1 ) - { - var aSplit = sAttr.split('.'); - nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1); - nNewNode.className = aSplit[1]; + if ( attr.indexOf('.') != -1 ) { + var split = attr.split('.'); + + id = split[0]; + className = split[1]; } - else if ( sAttr.charAt(0) == "#" ) - { - nNewNode.id = sAttr.substr(1, sAttr.length-1); + else if ( attr[0] == "#" ) { + id = attr; } - else - { - nNewNode.className = sAttr; + else { + className = attr; } - i += j; /* Move along the position array */ + newNode + .attr('id', id.substring(1)) + .addClass(className); + + i++; // Move along the position array } - insert.append( nNewNode ); - insert = $(nNewNode); + insert.append( newNode ); + insert = newNode; } - else if ( cOption == '>' ) - { - /* End container div */ + else if ( option == '>' ) { + // End container div insert = insert.parent(); } - // @todo Move options into their own plugins? - else if ( cOption == 'l' && features.bPaginate && features.bLengthChange ) - { - /* Length */ - featureNode = _fnFeatureHtmlLength( oSettings ); - } - else if ( cOption == 'f' && features.bFilter ) - { - /* Filter */ - featureNode = _fnFeatureHtmlFilter( oSettings ); - } - else if ( cOption == 'r' && features.bProcessing ) - { - /* pRocessing */ - featureNode = _fnFeatureHtmlProcessing( oSettings ); - } - else if ( cOption == 't' ) - { - /* Table */ - featureNode = _fnFeatureHtmlTable( oSettings ); - } - else if ( cOption == 'i' && features.bInfo ) - { - /* Info */ - featureNode = _fnFeatureHtmlInfo( oSettings ); - } - else if ( cOption == 'p' && features.bPaginate ) - { - /* Pagination */ - featureNode = _fnFeatureHtmlPaginate( oSettings ); + else if ( option == 't' ) { + // Table + featureNode = _fnFeatureHtmlTable( settings ); } - else if ( DataTable.ext.feature.length !== 0 ) + else { - /* Plug-in features */ - var aoFeatures = DataTable.ext.feature; - for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ ) - { - if ( cOption == aoFeatures[k].cFeature ) - { - featureNode = aoFeatures[k].fnInit( oSettings ); - break; + DataTable.ext.feature.forEach(function(feature) { + if ( option == feature.cFeature ) { + featureNode = feature.fnInit( settings ); } - } + }); } - /* Add to the 2D features array */ - if ( featureNode ) - { - var aanFeatures = oSettings.aanFeatures; - - if ( ! aanFeatures[cOption] ) - { - aanFeatures[cOption] = []; - } - - aanFeatures[cOption].push( featureNode ); + // Add to the display + if ( featureNode ) { insert.append( featureNode ); } } - - /* Built our DOM structure - replace the holding div with what we want */ - holding.replaceWith( insert ); - oSettings.nHolding = null; } @@ -3802,111 +3841,147 @@ * create a layout grid (array) of rows x columns, which contains a reference * to the cell that that point in the grid (regardless of col/rowspan), such that * any column / row could be removed and the new grid constructed - * @param array {object} aLayout Array to store the calculated layout in - * @param {node} nThead The header/footer element for the table + * @param {node} thead The header/footer element for the table + * @returns {array} Calculated layout array * @memberof DataTable#oApi */ - function _fnDetectHeader ( aLayout, nThead ) + function _fnDetectHeader ( settings, thead, write ) { - var nTrs = $(nThead).children('tr'); - var nTr, nCell; - var i, k, l, iLen, jLen, iColShifted, iColumn, iColspan, iRowspan; - var bUnique; - var fnShiftCol = function ( a, i, j ) { + var columns = settings.aoColumns; + var rows = $(thead).children('tr'); + var row, cell; + var i, k, l, iLen, shifted, column, colspan, rowspan; + var isHeader = thead && thead.nodeName.toLowerCase() === 'thead'; + var layout = []; + var unique; + var shift = function ( a, i, j ) { var k = a[i]; - while ( k[j] ) { + while ( k[j] ) { j++; } return j; }; - aLayout.splice( 0, aLayout.length ); - - /* We know how many rows there are in the layout - so prep it */ - for ( i=0, iLen=nTrs.length ; i<iLen ; i++ ) - { - aLayout.push( [] ); + // We know how many rows there are in the layout - so prep it + for ( i=0, iLen=rows.length ; i<iLen ; i++ ) { + layout.push( [] ); } - /* Calculate a layout array */ - for ( i=0, iLen=nTrs.length ; i<iLen ; i++ ) - { - nTr = nTrs[i]; - iColumn = 0; - - /* For every cell in the row... */ - nCell = nTr.firstChild; - while ( nCell ) { - if ( nCell.nodeName.toUpperCase() == "TD" || - nCell.nodeName.toUpperCase() == "TH" ) - { - /* Get the col and rowspan attributes from the DOM and sanitise them */ - iColspan = nCell.getAttribute('colspan') * 1; - iRowspan = nCell.getAttribute('rowspan') * 1; - iColspan = (!iColspan || iColspan===0 || iColspan===1) ? 1 : iColspan; - iRowspan = (!iRowspan || iRowspan===0 || iRowspan===1) ? 1 : iRowspan; - - /* There might be colspan cells already in this row, so shift our target - * accordingly - */ - iColShifted = fnShiftCol( aLayout, i, iColumn ); + for ( i=0, iLen=rows.length ; i<iLen ; i++ ) { + row = rows[i]; + column = 0; - /* Cache calculation for unique columns */ - bUnique = iColspan === 1 ? true : false; + // For every cell in the row.. + cell = row.firstChild; + while ( cell ) { + if ( + cell.nodeName.toUpperCase() == 'TD' || + cell.nodeName.toUpperCase() == 'TH' + ) { + var cols = []; + + // Get the col and rowspan attributes from the DOM and sanitise them + colspan = cell.getAttribute('colspan') * 1; + rowspan = cell.getAttribute('rowspan') * 1; + colspan = (!colspan || colspan===0 || colspan===1) ? 1 : colspan; + rowspan = (!rowspan || rowspan===0 || rowspan===1) ? 1 : rowspan; + + // There might be colspan cells already in this row, so shift our target + // accordingly + shifted = shift( layout, i, column ); + + // Cache calculation for unique columns + unique = colspan === 1 ? + true : + false; + + // Perform header setup + if ( write ) { + if (unique) { + // Allow column options to be set from HTML attributes + _fnColumnOptions( settings, shifted, $(cell).data() ); + + // Get the width for the column. This can be defined from the + // width attribute, style attribute or `columns.width` option + var columnDef = columns[shifted]; + var width = cell.getAttribute('width') || null; + var t = cell.style.width.match(/width:\s*(\d+[pxem%]+)/); + if ( t ) { + width = t[1]; + } - /* If there is col / rowspan, copy the information into the layout grid */ - for ( l=0 ; l<iColspan ; l++ ) - { - for ( k=0 ; k<iRowspan ; k++ ) - { - aLayout[i+k][iColShifted+l] = { - "cell": nCell, - "unique": bUnique - }; - aLayout[i+k].nTr = nTr; + columnDef.sWidthOrig = columnDef.sWidth || width; + + if (isHeader) { + // Column title handling - can be user set, or read from the DOM + // This happens before the render, so the original is still in place + if ( columnDef.sTitle !== null && ! columnDef.autoTitle ) { + cell.innerHTML = columnDef.sTitle; + } + + if (! columnDef.sTitle && unique) { + columnDef.sTitle = _stripHtml(cell.innerHTML); + columnDef.autoTitle = true; + } + } + else { + // Footer specific operations + if (columnDef.footer) { + cell.innerHTML = columnDef.footer; + } + } + + // Fall back to the aria-label attribute on the table header if no ariaTitle is + // provided. + if (! columnDef.ariaTitle) { + columnDef.ariaTitle = $(cell).attr("aria-label") || columnDef.sTitle; + } + + // Column specific class names + if ( columnDef.className ) { + $(cell).addClass( columnDef.className ); + } + } + + // Wrap the column title so we can write to it in future + if ( $('span.dt-column-title', cell).length === 0) { + $('<span>') + .addClass('dt-column-title') + .append(cell.childNodes) + .appendTo(cell); + } + + if ( isHeader && $('span.dt-column-order', cell).length === 0) { + $('<span>') + .addClass('dt-column-order') + .appendTo(cell); } } - } - nCell = nCell.nextSibling; - } - } - } + // If there is col / rowspan, copy the information into the layout grid + for ( l=0 ; l<colspan ; l++ ) { + for ( k=0 ; k<rowspan ; k++ ) { + layout[i+k][shifted+l] = { + cell: cell, + unique: unique + }; - /** - * Get an array of unique th elements, one for each column - * @param {object} oSettings dataTables settings object - * @param {node} nHeader automatically detect the layout from this node - optional - * @param {array} aLayout thead/tfoot layout from _fnDetectHeader - optional - * @returns array {node} aReturn list of unique th's - * @memberof DataTable#oApi - */ - function _fnGetUniqueThs ( oSettings, nHeader, aLayout ) - { - var aReturn = []; - if ( !aLayout ) - { - aLayout = oSettings.aoHeader; - if ( nHeader ) - { - aLayout = []; - _fnDetectHeader( aLayout, nHeader ); - } - } + layout[i+k].row = row; + } - for ( var i=0, iLen=aLayout.length ; i<iLen ; i++ ) - { - for ( var j=0, jLen=aLayout[i].length ; j<jLen ; j++ ) - { - if ( aLayout[i][j].unique && - (!aReturn[j] || !oSettings.bSortCellsTop) ) - { - aReturn[j] = aLayout[i][j].cell; + cols.push( shifted+l ); + } + + // Assign an attribute so spanning cells can still be identified + // as belonging to a column + cell.setAttribute('data-dt-column', _unique(cols).join(',')); } + + cell = cell.nextSibling; } } - return aReturn; + return layout; } /** @@ -3942,34 +4017,6 @@ */ function _fnBuildAjax( oSettings, data, fn ) { - // Compatibility with 1.9-, allow fnServerData and event to manipulate - _fnCallbackFire( oSettings, 'aoServerParams', 'serverParams', [data] ); - - // Convert to object based for 1.10+ if using the old array scheme which can - // come from server-side processing or serverParams - if ( data && Array.isArray(data) ) { - var tmp = {}; - var rbracket = /(.*?)\[\]$/; - - $.each( data, function (key, val) { - var match = val.name.match(rbracket); - - if ( match ) { - // Support for arrays - var name = match[0]; - - if ( ! tmp[ name ] ) { - tmp[ name ] = []; - } - tmp[ name ].push( val.value ); - } - else { - tmp[val.name] = val.value; - } - } ); - data = tmp; - } - var ajaxData; var ajax = oSettings.ajax; var instance = oSettings.oInstance; @@ -3990,7 +4037,7 @@ oSettings.json = json; - _fnCallbackFire( oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR] ); + _fnCallbackFire( oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR], true ); fn( json ); }; @@ -4013,15 +4060,18 @@ } var baseAjax = { + "url": typeof ajax === 'string' ? + ajax : + '', "data": data, "success": callback, "dataType": "json", "cache": false, "type": oSettings.sServerMethod, - "error": function (xhr, error, thrown) { - var ret = _fnCallbackFire( oSettings, null, 'xhr', [oSettings, null, oSettings.jqXHR] ); + "error": function (xhr, error) { + var ret = _fnCallbackFire( oSettings, null, 'xhr', [oSettings, null, oSettings.jqXHR], true ); - if ( $.inArray( true, ret ) === -1 ) { + if ( ret.indexOf(true) === -1 ) { if ( error == "parsererror" ) { _fnLog( oSettings, 0, 'Invalid JSON response', 1 ); } @@ -4034,43 +4084,38 @@ } }; + // If `ajax` option is an object, extend and override our default base + if ( $.isPlainObject( ajax ) ) { + $.extend( baseAjax, ajax ) + } + // Store the data submitted for the API oSettings.oAjaxData = data; // Allow plug-ins and external processes to modify the data - _fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data] ); + _fnCallbackFire( oSettings, null, 'preXhr', [oSettings, data, baseAjax], true ); - if ( oSettings.fnServerData ) - { - // DataTables 1.9- compatibility - oSettings.fnServerData.call( instance, - oSettings.sAjaxSource, - $.map( data, function (val, key) { // Need to convert back to 1.9 trad format - return { name: key, value: val }; - } ), - callback, - oSettings - ); - } - else if ( oSettings.sAjaxSource || typeof ajax === 'string' ) - { - // DataTables 1.9- compatibility - oSettings.jqXHR = $.ajax( $.extend( baseAjax, { - url: ajax || oSettings.sAjaxSource - } ) ); - } - else if ( typeof ajax === 'function' ) + if ( typeof ajax === 'function' ) { // Is a function - let the caller define what needs to be done oSettings.jqXHR = ajax.call( instance, data, callback, oSettings ); } - else - { + else if (ajax.url === '') { + // No url, so don't load any data. Just apply an empty data array + // to the object for the callback. + var empty = {}; + + DataTable.util.set(ajax.dataSrc)(empty, []); + callback(empty); + } + else { // Object to extend the base settings - oSettings.jqXHR = $.ajax( $.extend( baseAjax, ajax ) ); + oSettings.jqXHR = $.ajax( baseAjax ); // Restore for next time around - ajax.data = ajaxData; + if ( ajaxData ) { + ajax.data = ajaxData; + } } } @@ -4086,16 +4131,11 @@ settings.iDraw++; _fnProcessingDisplay( settings, true ); - // Keep track of drawHold state to handle scrolling after the Ajax call - var drawHold = settings._drawHold; - _fnBuildAjax( settings, _fnAjaxParameters( settings ), function(json) { - settings._drawHold = drawHold; _fnAjaxUpdateDraw( settings, json ); - settings._drawHold = false; } ); } @@ -4103,11 +4143,7 @@ /** * Build up the parameters in an object needed for a server-side processing - * request. Note that this is basically done twice, is different ways - a modern - * method which is used by default in DataTables 1.10 which uses objects and - * arrays, or the 1.9- method with is name / value pairs. 1.9 method is used if - * the sAjaxSource option is used in the initialisation, or the legacyAjax - * option is set. + * request. * @param {object} oSettings dataTables settings object * @returns {bool} block the table drawing or not * @memberof DataTable#oApi @@ -4116,96 +4152,57 @@ { var columns = settings.aoColumns, - columnCount = columns.length, features = settings.oFeatures, preSearch = settings.oPreviousSearch, preColSearch = settings.aoPreSearchCols, - i, data = [], dataProp, column, columnSearch, - sort = _fnSortFlatten( settings ), - displayStart = settings._iDisplayStart, - displayLength = features.bPaginate !== false ? + colData = function ( idx, prop ) { + return typeof columns[idx][prop] === 'function' ? + 'function' : + columns[idx][prop]; + }; + + return { + draw: settings.iDraw, + columns: columns.map( function ( column, i ) { + return { + data: colData(i, 'mData'), + name: column.sName, + searchable: column.bSearchable, + orderable: column.bSortable, + search: { + value: preColSearch[i].search, + regex: preColSearch[i].regex, + fixed: Object.keys(column.searchFixed).map( function(name) { + return { + name: name, + term: column.searchFixed[name].toString() + } + }) + } + }; + } ), + order: _fnSortFlatten( settings ).map( function ( val ) { + return { + column: val.col, + dir: val.dir, + name: colData(val.col, 'sName') + }; + } ), + start: settings._iDisplayStart, + length: features.bPaginate ? settings._iDisplayLength : - -1; - - var param = function ( name, value ) { - data.push( { 'name': name, 'value': value } ); - }; - - // DataTables 1.9- compatible method - param( 'sEcho', settings.iDraw ); - param( 'iColumns', columnCount ); - param( 'sColumns', _pluck( columns, 'sName' ).join(',') ); - param( 'iDisplayStart', displayStart ); - param( 'iDisplayLength', displayLength ); - - // DataTables 1.10+ method - var d = { - draw: settings.iDraw, - columns: [], - order: [], - start: displayStart, - length: displayLength, - search: { - value: preSearch.sSearch, - regex: preSearch.bRegex + -1, + search: { + value: preSearch.search, + regex: preSearch.regex, + fixed: Object.keys(settings.searchFixed).map( function(name) { + return { + name: name, + term: settings.searchFixed[name].toString() + } + }) } }; - - for ( i=0 ; i<columnCount ; i++ ) { - column = columns[i]; - columnSearch = preColSearch[i]; - dataProp = typeof column.mData=="function" ? 'function' : column.mData ; - - d.columns.push( { - data: dataProp, - name: column.sName, - searchable: column.bSearchable, - orderable: column.bSortable, - search: { - value: columnSearch.sSearch, - regex: columnSearch.bRegex - } - } ); - - param( "mDataProp_"+i, dataProp ); - - if ( features.bFilter ) { - param( 'sSearch_'+i, columnSearch.sSearch ); - param( 'bRegex_'+i, columnSearch.bRegex ); - param( 'bSearchable_'+i, column.bSearchable ); - } - - if ( features.bSort ) { - param( 'bSortable_'+i, column.bSortable ); - } - } - - if ( features.bFilter ) { - param( 'sSearch', preSearch.sSearch ); - param( 'bRegex', preSearch.bRegex ); - } - - if ( features.bSort ) { - $.each( sort, function ( i, val ) { - d.order.push( { column: val.col, dir: val.dir } ); - - param( 'iSortCol_'+i, val.col ); - param( 'sSortDir_'+i, val.dir ); - } ); - - param( 'iSortingCols', sort.length ); - } - - // If the legacy.ajax parameter is null, then we automatically decide which - // form to use, based on sAjaxSource - var legacy = DataTable.ext.legacy.ajax; - if ( legacy === null ) { - return settings.sAjaxSource ? data : d; - } - - // Otherwise, if legacy has been specified then we use that to decide on the - // form - return legacy ? data : d; } @@ -4222,16 +4219,10 @@ */ function _fnAjaxUpdateDraw ( settings, json ) { - // v1.10 uses camelCase variables, while 1.9 uses Hungarian notation. - // Support both - var compat = function ( old, modern ) { - return json[old] !== undefined ? json[old] : json[modern]; - }; - - var data = _fnAjaxDataSrc( settings, json ); - var draw = compat( 'sEcho', 'draw' ); - var recordsTotal = compat( 'iTotalRecords', 'recordsTotal' ); - var recordsFiltered = compat( 'iTotalDisplayRecords', 'recordsFiltered' ); + var data = _fnAjaxDataSrc(settings, json); + var draw = _fnAjaxDataSrcParam(settings, 'draw', json); + var recordsTotal = _fnAjaxDataSrcParam(settings, 'recordsTotal', json); + var recordsFiltered = _fnAjaxDataSrcParam(settings, 'recordsFiltered', json); if ( draw !== undefined ) { // Protect against out of sequence returns @@ -4256,11 +4247,7 @@ settings.aiDisplay = settings.aiDisplayMaster.slice(); _fnDraw( settings, true ); - - if ( ! settings._bInitComplete ) { - _fnInitComplete( settings, json ); - } - + _fnInitComplete( settings ); _fnProcessingDisplay( settings, false ); } @@ -4269,188 +4256,144 @@ * Get the data from the JSON data source to use for drawing a table. Using * `_fnGetObjectDataFn` allows the data to be sourced from a property of the * source object, or from a processing function. - * @param {object} oSettings dataTables settings object + * @param {object} settings dataTables settings object * @param {object} json Data source object / array from the server * @return {array} Array of data to use */ - function _fnAjaxDataSrc ( oSettings, json, write ) - { - var dataSrc = $.isPlainObject( oSettings.ajax ) && oSettings.ajax.dataSrc !== undefined ? - oSettings.ajax.dataSrc : - oSettings.sAjaxDataProp; // Compatibility with 1.9-. + function _fnAjaxDataSrc ( settings, json, write ) + { + var dataProp = 'data'; + + if ($.isPlainObject( settings.ajax ) && settings.ajax.dataSrc !== undefined) { + // Could in inside a `dataSrc` object, or not! + var dataSrc = settings.ajax.dataSrc; + + // string, function and object are valid types + if (typeof dataSrc === 'string' || typeof dataSrc === 'function') { + dataProp = dataSrc; + } + else if (dataSrc.data !== undefined) { + dataProp = dataSrc.data; + } + } if ( ! write ) { - if ( dataSrc === 'data' ) { + if ( dataProp === 'data' ) { // If the default, then we still want to support the old style, and safely ignore // it if possible - return json.aaData || json[dataSrc]; + return json.aaData || json[dataProp]; } - return dataSrc !== "" ? - _fnGetObjectDataFn( dataSrc )( json ) : + return dataProp !== "" ? + _fnGetObjectDataFn( dataProp )( json ) : json; } - + // set - _fnSetObjectDataFn( dataSrc )( json, write ); + _fnSetObjectDataFn( dataProp )( json, write ); } /** - * Generate the node required for filtering text - * @returns {node} Filter control element - * @param {object} oSettings dataTables settings object - * @memberof DataTable#oApi + * Very similar to _fnAjaxDataSrc, but for the other SSP properties + * @param {*} settings DataTables settings object + * @param {*} param Target parameter + * @param {*} json JSON data + * @returns Resolved value */ - function _fnFeatureHtmlFilter ( settings ) - { - var classes = settings.oClasses; - var tableId = settings.sTableId; - var language = settings.oLanguage; - var previousSearch = settings.oPreviousSearch; - var features = settings.aanFeatures; - var input = '<input type="search" class="'+classes.sFilterInput+'"/>'; - - var str = language.sSearch; - str = str.match(/_INPUT_/) ? - str.replace('_INPUT_', input) : - str+input; + function _fnAjaxDataSrcParam (settings, param, json) { + var dataSrc = $.isPlainObject( settings.ajax ) + ? settings.ajax.dataSrc + : null; - var filter = $('<div/>', { - 'id': ! features.f ? tableId+'_filter' : null, - 'class': classes.sFilter - } ) - .append( $('<label/>' ).append( str ) ); - - var searchFn = function(event) { - /* Update all other filter input elements for the new display */ - var n = features.f; - var val = !this.value ? "" : this.value; // mental IE8 fix :-( - if(previousSearch.return && event.key !== "Enter") { - return; - } - /* Now do the filter */ - if ( val != previousSearch.sSearch ) { - _fnFilterComplete( settings, { - "sSearch": val, - "bRegex": previousSearch.bRegex, - "bSmart": previousSearch.bSmart , - "bCaseInsensitive": previousSearch.bCaseInsensitive, - "return": previousSearch.return - } ); - - // Need to redraw, without resorting - settings._iDisplayStart = 0; - _fnDraw( settings ); - } - }; - - var searchDelay = settings.searchDelay !== null ? - settings.searchDelay : - _fnDataSource( settings ) === 'ssp' ? - 400 : - 0; + if (dataSrc && dataSrc[param]) { + // Get from custom location + return _fnGetObjectDataFn( dataSrc[param] )( json ); + } - var jqFilter = $('input', filter) - .val( previousSearch.sSearch ) - .attr( 'placeholder', language.sSearchPlaceholder ) - .on( - 'keyup.DT search.DT input.DT paste.DT cut.DT', - searchDelay ? - _fnThrottle( searchFn, searchDelay ) : - searchFn - ) - .on( 'mouseup.DT', function(e) { - // Edge fix! Edge 17 does not trigger anything other than mouse events when clicking - // on the clear icon (Edge bug 17584515). This is safe in other browsers as `searchFn` - // checks the value to see if it has changed. In other browsers it won't have. - setTimeout( function () { - searchFn.call(jqFilter[0], e); - }, 10); - } ) - .on( 'keypress.DT', function(e) { - /* Prevent form submission */ - if ( e.keyCode == 13 ) { - return false; - } - } ) - .attr('aria-controls', tableId); + // else - Default behaviour + var old = ''; - // Update the input elements whenever the table is filtered - $(settings.nTable).on( 'search.dt.DT', function ( ev, s ) { - if ( settings === s ) { - // IE9 throws an 'unknown error' if document.activeElement is used - // inside an iframe or frame... - try { - if ( jqFilter[0] !== document.activeElement ) { - jqFilter.val( previousSearch.sSearch ); - } - } - catch ( e ) {} - } - } ); + // Legacy support + if (param === 'draw') { + old = 'sEcho'; + } + else if (param === 'recordsTotal') { + old = 'iTotalRecords'; + } + else if (param === 'recordsFiltered') { + old = 'iTotalDisplayRecords'; + } - return filter[0]; + return json[old] !== undefined + ? json[old] + : json[param]; } /** * Filter the table using both the global filter and column based filtering - * @param {object} oSettings dataTables settings object - * @param {object} oSearch search information - * @param {int} [iForce] force a research of the master array (1) or not (undefined or 0) + * @param {object} settings dataTables settings object + * @param {object} input search information * @memberof DataTable#oApi */ - function _fnFilterComplete ( oSettings, oInput, iForce ) + function _fnFilterComplete ( settings, input ) { - var oPrevSearch = oSettings.oPreviousSearch; - var aoPrevSearch = oSettings.aoPreSearchCols; - var fnSaveFilter = function ( oFilter ) { - /* Save the filtering values */ - oPrevSearch.sSearch = oFilter.sSearch; - oPrevSearch.bRegex = oFilter.bRegex; - oPrevSearch.bSmart = oFilter.bSmart; - oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive; - oPrevSearch.return = oFilter.return; - }; - var fnRegex = function ( o ) { - // Backwards compatibility with the bEscapeRegex option - return o.bEscapeRegex !== undefined ? !o.bEscapeRegex : o.bRegex; - }; + var columnsSearch = settings.aoPreSearchCols; // Resolve any column types that are unknown due to addition or invalidation // @todo As per sort - can this be moved into an event handler? - _fnColumnTypes( oSettings ); + _fnColumnTypes( settings ); - /* In server-side processing all filtering is done by the server, so no point hanging around here */ - if ( _fnDataSource( oSettings ) != 'ssp' ) + // In server-side processing all filtering is done by the server, so no point hanging around here + if ( _fnDataSource( settings ) != 'ssp' ) { - /* Global filter */ - _fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive ); - fnSaveFilter( oInput ); + // Check if any of the rows were invalidated + _fnFilterData( settings ); + + // Start from the full data set + settings.aiDisplay = settings.aiDisplayMaster.slice(); + + // Global filter first + _fnFilter( settings.aiDisplay, settings, input.search, input ); + + $.each(settings.searchFixed, function (name, term) { + _fnFilter(settings.aiDisplay, settings, term, {}); + }); - /* Now do the individual column filter */ - for ( var i=0 ; i<aoPrevSearch.length ; i++ ) + // Then individual column filters + for ( var i=0 ; i<columnsSearch.length ; i++ ) { - _fnFilterColumn( oSettings, aoPrevSearch[i].sSearch, i, fnRegex(aoPrevSearch[i]), - aoPrevSearch[i].bSmart, aoPrevSearch[i].bCaseInsensitive ); + var col = columnsSearch[i]; + + _fnFilter( + settings.aiDisplay, + settings, + col.search, + col, + i + ); + + $.each(settings.aoColumns[i].searchFixed, function (name, term) { + _fnFilter(settings.aiDisplay, settings, term, {}, i); + }); } - /* Custom filtering */ - _fnFilterCustom( oSettings ); - } - else - { - fnSaveFilter( oInput ); + // And finally global filtering + _fnFilterCustom( settings ); } - /* Tell the draw function we have been filtering */ - oSettings.bFiltered = true; - _fnCallbackFire( oSettings, null, 'search', [oSettings] ); + // Tell the draw function we have been filtering + settings.bFiltered = true; + + _fnCallbackFire( settings, null, 'search', [settings] ); } /** * Apply custom filtering functions + * + * This is legacy now that we have named functions, but it is widely used + * from 1.x, so it is not yet deprecated. * @param {object} oSettings dataTables settings object * @memberof DataTable#oApi */ @@ -4476,97 +4419,49 @@ // So the array reference doesn't break set the results into the // existing array displayRows.length = 0; - $.merge( displayRows, rows ); + displayRows.push.apply(displayRows, rows); } } /** - * Filter the table on a per-column basis - * @param {object} oSettings dataTables settings object - * @param {string} sInput string to filter on - * @param {int} iColumn column to filter - * @param {bool} bRegex treat search string as a regular expression or not - * @param {bool} bSmart use smart filtering or not - * @param {bool} bCaseInsensitive Do case insensitive matching or not - * @memberof DataTable#oApi + * Filter the data table based on user input and draw the table */ - function _fnFilterColumn ( settings, searchStr, colIdx, regex, smart, caseInsensitive ) + function _fnFilter( searchRows, settings, input, options, column ) { - if ( searchStr === '' ) { + if ( input === '' ) { return; } - var data; - var out = []; - var display = settings.aiDisplay; - var rpSearch = _fnFilterCreateSearch( searchStr, regex, smart, caseInsensitive ); - - for ( var i=0 ; i<display.length ; i++ ) { - data = settings.aoData[ display[i] ]._aFilterData[ colIdx ]; - - if ( rpSearch.test( data ) ) { - out.push( display[i] ); - } - } - - settings.aiDisplay = out; - } - + var i = 0; + var matched = []; - /** - * Filter the data table based on user input and draw the table - * @param {object} settings dataTables settings object - * @param {string} input string to filter on - * @param {int} force optional - force a research of the master array (1) or not (undefined or 0) - * @param {bool} regex treat as a regular expression or not - * @param {bool} smart perform smart filtering or not - * @param {bool} caseInsensitive Do case insensitive matching or not - * @memberof DataTable#oApi - */ - function _fnFilter( settings, input, force, regex, smart, caseInsensitive ) - { - var rpSearch = _fnFilterCreateSearch( input, regex, smart, caseInsensitive ); - var prevSearch = settings.oPreviousSearch.sSearch; - var displayMaster = settings.aiDisplayMaster; - var display, invalidated, i; - var filtered = []; - - // Need to take account of custom filtering functions - always filter - if ( DataTable.ext.search.length !== 0 ) { - force = true; - } + // Search term can be a function, regex or string - if a string we apply our + // smart filtering regex (assuming the options require that) + var searchFunc = typeof input === 'function' ? input : null; + var rpSearch = input instanceof RegExp + ? input + : searchFunc + ? null + : _fnFilterCreateSearch( input, options ); - // Check if any of the rows were invalidated - invalidated = _fnFilterData( settings ); + // Then for each row, does the test pass. If not, lop the row from the array + for (i=0 ; i<searchRows.length ; i++) { + var row = settings.aoData[ searchRows[i] ]; + var data = column === undefined + ? row._sFilterRow + : row._aFilterData[ column ]; - // If the input is blank - we just want the full data set - if ( input.length <= 0 ) { - settings.aiDisplay = displayMaster.slice(); - } - else { - // New search - start from the master array - if ( invalidated || - force || - regex || - prevSearch.length > input.length || - input.indexOf(prevSearch) !== 0 || - settings.bSorted // On resort, the display master needs to be - // re-filtered since indexes will have changed - ) { - settings.aiDisplay = displayMaster.slice(); + if ( (searchFunc && searchFunc(data, row._aData, searchRows[i], column)) || (rpSearch && rpSearch.test(data)) ) { + matched.push(searchRows[i]); } + } - // Search the display array - display = settings.aiDisplay; - - for ( i=0 ; i<display.length ; i++ ) { - if ( rpSearch.test( settings.aoData[ display[i] ]._sFilterRow ) ) { - filtered.push( display[i] ); - } - } + // Mutate the searchRows array + searchRows.length = matched.length; - settings.aiDisplay = filtered; + for (i=0 ; i<matched.length ; i++) { + searchRows[i] = matched[i]; } } @@ -4580,37 +4475,92 @@ * @returns {RegExp} constructed object * @memberof DataTable#oApi */ - function _fnFilterCreateSearch( search, regex, smart, caseInsensitive ) + function _fnFilterCreateSearch( search, inOpts ) { - search = regex ? + var not = []; + var options = $.extend({}, { + boundary: false, + caseInsensitive: true, + exact: false, + regex: false, + smart: true + }, inOpts); + + if (typeof search !== 'string') { + search = search.toString(); + } + + // Remove diacritics if normalize is set up to do so + search = _normalize(search); + + if (options.exact) { + return new RegExp( + '^'+_fnEscapeRegex(search)+'$', + options.caseInsensitive ? 'i' : '' + ); + } + + search = options.regex ? search : _fnEscapeRegex( search ); - if ( smart ) { + if ( options.smart ) { /* For smart filtering we want to allow the search to work regardless of * word order. We also want double quoted text to be preserved, so word - * order is important - a la google. So this is what we want to - * generate: + * order is important - a la google. And a negative look around for + * finding rows which don't contain a given string. + * + * So this is the sort of thing we want to generate: * * ^(?=.*?\bone\b)(?=.*?\btwo three\b)(?=.*?\bfour\b).*$ */ - var a = $.map( search.match( /["\u201C][^"\u201D]+["\u201D]|[^ ]+/g ) || [''], function ( word ) { + var parts = search.match( /!?["\u201C][^"\u201D]+["\u201D]|[^ ]+/g ) || ['']; + var a = parts.map( function ( word ) { + var negative = false; + var m; + + // Determine if it is a "does not include" + if ( word.charAt(0) === '!' ) { + negative = true; + word = word.substring(1); + } + + // Strip the quotes from around matched phrases if ( word.charAt(0) === '"' ) { - var m = word.match( /^"(.*)"$/ ); + m = word.match( /^"(.*)"$/ ); word = m ? m[1] : word; } else if ( word.charAt(0) === '\u201C' ) { - var m = word.match( /^\u201C(.*)\u201D$/ ); + // Smart quote match (iPhone users) + m = word.match( /^\u201C(.*)\u201D$/ ); word = m ? m[1] : word; } - return word.replace('"', ''); + // For our "not" case, we need to modify the string that is + // allowed to match at the end of the expression. + if (negative) { + if (word.length > 1) { + not.push('(?!'+word+')'); + } + + word = ''; + } + + return word.replace(/"/g, ''); } ); - search = '^(?=.*?'+a.join( ')(?=.*?' )+').*$'; + var match = not.length + ? not.join('') + : ''; + + var boundary = options.boundary + ? '\\b' + : ''; + + search = '^(?=.*?'+boundary+a.join( ')(?=.*?'+boundary )+')('+match+'.)*$'; } - return new RegExp( search, caseInsensitive ? 'i' : '' ); + return new RegExp( search, options.caseInsensitive ? 'i' : '' ); } @@ -4629,12 +4579,17 @@ function _fnFilterData ( settings ) { var columns = settings.aoColumns; + var data = settings.aoData; var column; - var i, j, ien, jen, filterData, cellData, row; + var j, jen, filterData, cellData, row; var wasInvalidated = false; - for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) { - row = settings.aoData[i]; + for ( var rowIdx=0 ; rowIdx<data.length ; rowIdx++ ) { + if (! data[rowIdx]) { + continue; + } + + row = data[rowIdx]; if ( ! row._aFilterData ) { filterData = []; @@ -4643,10 +4598,9 @@ column = columns[j]; if ( column.bSearchable ) { - cellData = _fnGetCellData( settings, i, j, 'filter' ); + cellData = _fnGetCellData( settings, rowIdx, j, 'filter' ); - // Search in DataTables 1.10 is string based. In 1.11 this - // should be altered to also allow strict type checking. + // Search in DataTables is string based if ( cellData === null ) { cellData = ''; } @@ -4662,7 +4616,7 @@ // If it looks like there is an HTML entity in the string, // attempt to decode it so sorting works as expected. Note that // we could use a single line of jQuery to do this, but the DOM - // method used here is much faster http://jsperf.com/html-decode + // method used here is much faster https://jsperf.com/html-decode if ( cellData.indexOf && cellData.indexOf('&') !== -1 ) { __filter_div.innerHTML = cellData; cellData = __filter_div_textContent ? @@ -4688,152 +4642,13 @@ /** - * Convert from the internal Hungarian notation to camelCase for external - * interaction - * @param {object} obj Object to convert - * @returns {object} Inverted object - * @memberof DataTable#oApi - */ - function _fnSearchToCamel ( obj ) - { - return { - search: obj.sSearch, - smart: obj.bSmart, - regex: obj.bRegex, - caseInsensitive: obj.bCaseInsensitive - }; - } - - - - /** - * Convert from camelCase notation to the internal Hungarian. We could use the - * Hungarian convert function here, but this is cleaner - * @param {object} obj Object to convert - * @returns {object} Inverted object - * @memberof DataTable#oApi - */ - function _fnSearchToHung ( obj ) - { - return { - sSearch: obj.search, - bSmart: obj.smart, - bRegex: obj.regex, - bCaseInsensitive: obj.caseInsensitive - }; - } - - /** - * Generate the node required for the info display - * @param {object} oSettings dataTables settings object - * @returns {node} Information element - * @memberof DataTable#oApi - */ - function _fnFeatureHtmlInfo ( settings ) - { - var - tid = settings.sTableId, - nodes = settings.aanFeatures.i, - n = $('<div/>', { - 'class': settings.oClasses.sInfo, - 'id': ! nodes ? tid+'_info' : null - } ); - - if ( ! nodes ) { - // Update display on each draw - settings.aoDrawCallback.push( { - "fn": _fnUpdateInfo, - "sName": "information" - } ); - - n - .attr( 'role', 'status' ) - .attr( 'aria-live', 'polite' ); - - // Table is described by our info div - $(settings.nTable).attr( 'aria-describedby', tid+'_info' ); - } - - return n[0]; - } - - - /** - * Update the information elements in the display - * @param {object} settings dataTables settings object - * @memberof DataTable#oApi - */ - function _fnUpdateInfo ( settings ) - { - /* Show information about the table */ - var nodes = settings.aanFeatures.i; - if ( nodes.length === 0 ) { - return; - } - - var - lang = settings.oLanguage, - start = settings._iDisplayStart+1, - end = settings.fnDisplayEnd(), - max = settings.fnRecordsTotal(), - total = settings.fnRecordsDisplay(), - out = total ? - lang.sInfo : - lang.sInfoEmpty; - - if ( total !== max ) { - /* Record set after filtering */ - out += ' ' + lang.sInfoFiltered; - } - - // Convert the macros - out += lang.sInfoPostFix; - out = _fnInfoMacros( settings, out ); - - var callback = lang.fnInfoCallback; - if ( callback !== null ) { - out = callback.call( settings.oInstance, - settings, start, end, max, total, out - ); - } - - $(nodes).html( out ); - } - - - function _fnInfoMacros ( settings, str ) - { - // When infinite scrolling, we are always starting at 1. _iDisplayStart is used only - // internally - var - formatter = settings.fnFormatNumber, - start = settings._iDisplayStart+1, - len = settings._iDisplayLength, - vis = settings.fnRecordsDisplay(), - all = len === -1; - - return str. - replace(/_START_/g, formatter.call( settings, start ) ). - replace(/_END_/g, formatter.call( settings, settings.fnDisplayEnd() ) ). - replace(/_MAX_/g, formatter.call( settings, settings.fnRecordsTotal() ) ). - replace(/_TOTAL_/g, formatter.call( settings, vis ) ). - replace(/_PAGE_/g, formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ). - replace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) ); - } - - - - /** * Draw the table for the first time, adding all required features * @param {object} settings dataTables settings object * @memberof DataTable#oApi */ function _fnInitialise ( settings ) { - var i, iLen, iAjaxStart=settings.iInitDisplayStart; - var columns = settings.aoColumns, column; - var features = settings.oFeatures; - var deferLoading = settings.bDeferLoading; // value modified by the draw + var i, iAjaxStart=settings.iInitDisplayStart; /* Ensure that the table data is fully initialised */ if ( ! settings.bInitialised ) { @@ -4841,31 +4656,22 @@ return; } - /* Show the display HTML options */ - _fnAddOptionsHtml( settings ); - /* Build and draw the header / footer for the table */ - _fnBuildHead( settings ); + _fnBuildHead( settings, 'header' ); + _fnBuildHead( settings, 'footer' ); _fnDrawHead( settings, settings.aoHeader ); _fnDrawHead( settings, settings.aoFooter ); - /* Okay to show that something is going on now */ - _fnProcessingDisplay( settings, true ); - - /* Calculate sizes for columns */ - if ( features.bAutoWidth ) { - _fnCalculateColumnWidths( settings ); - } + // Enable features + _fnAddOptionsHtml( settings ); + _fnSortInit( settings ); - for ( i=0, iLen=columns.length ; i<iLen ; i++ ) { - column = columns[i]; + _colGroup( settings ); - if ( column.sWidth ) { - column.nTh.style.width = _fnStringToCss( column.sWidth ); - } - } + /* Okay to show that something is going on now */ + _fnProcessingDisplay( settings, true ); - _fnCallbackFire( settings, null, 'preInit', [settings] ); + _fnCallbackFire( settings, null, 'preInit', [settings], true ); // If there is default sorting required - let's do it. The sort function // will do the drawing for us. Otherwise we draw the table regardless of the @@ -4873,12 +4679,13 @@ // data (show 'loading' message possibly) _fnReDraw( settings ); + var dataSrc = _fnDataSource( settings ); + // Server-side processing init complete is done by _fnAjaxUpdateDraw - var dataSrc = _fnDataSource( settings ); - if ( dataSrc != 'ssp' || deferLoading ) { + if ( dataSrc != 'ssp' ) { // if there is an ajax source load the data if ( dataSrc == 'ajax' ) { - _fnBuildAjax( settings, [], function(json) { + _fnBuildAjax( settings, {}, function(json) { var aData = _fnAjaxDataSrc( settings, json ); // Got the data - add it to the table @@ -4892,14 +4699,13 @@ settings.iInitDisplayStart = iAjaxStart; _fnReDraw( settings ); - _fnProcessingDisplay( settings, false ); - _fnInitComplete( settings, json ); + _fnInitComplete( settings ); }, settings ); } else { - _fnProcessingDisplay( settings, false ); _fnInitComplete( settings ); + _fnProcessingDisplay( settings, false ); } } } @@ -4907,26 +4713,27 @@ /** * Draw the table for the first time, adding all required features - * @param {object} oSettings dataTables settings object - * @param {object} [json] JSON from the server that completed the table, if using Ajax source - * with client-side processing (optional) + * @param {object} settings dataTables settings object * @memberof DataTable#oApi */ - function _fnInitComplete ( settings, json ) + function _fnInitComplete ( settings ) { + if (settings._bInitComplete) { + return; + } + + var args = [settings, settings.json]; + settings._bInitComplete = true; - // When data was added after the initialisation (data or Ajax) we need to - // calculate the column sizing - if ( json || settings.oInit.aaData ) { - _fnAdjustColumnSizing( settings ); - } + // Table is fully set up and we have data, so calculate the + // column widths + _fnAdjustColumnSizing( settings ); - _fnCallbackFire( settings, null, 'plugin-init', [settings, json] ); - _fnCallbackFire( settings, 'aoInitComplete', 'init', [settings, json] ); + _fnCallbackFire( settings, null, 'plugin-init', args, true ); + _fnCallbackFire( settings, 'aoInitComplete', 'init', args, true ); } - function _fnLengthChange ( settings, val ) { var len = parseInt( val, 10 ); @@ -4938,131 +4745,6 @@ _fnCallbackFire( settings, null, 'length', [settings, len] ); } - - /** - * Generate the node required for user display length changing - * @param {object} settings dataTables settings object - * @returns {node} Display length feature node - * @memberof DataTable#oApi - */ - function _fnFeatureHtmlLength ( settings ) - { - var - classes = settings.oClasses, - tableId = settings.sTableId, - menu = settings.aLengthMenu, - d2 = Array.isArray( menu[0] ), - lengths = d2 ? menu[0] : menu, - language = d2 ? menu[1] : menu; - - var select = $('<select/>', { - 'name': tableId+'_length', - 'aria-controls': tableId, - 'class': classes.sLengthSelect - } ); - - for ( var i=0, ien=lengths.length ; i<ien ; i++ ) { - select[0][ i ] = new Option( - typeof language[i] === 'number' ? - settings.fnFormatNumber( language[i] ) : - language[i], - lengths[i] - ); - } - - var div = $('<div><label/></div>').addClass( classes.sLength ); - if ( ! settings.aanFeatures.l ) { - div[0].id = tableId+'_length'; - } - - div.children().append( - settings.oLanguage.sLengthMenu.replace( '_MENU_', select[0].outerHTML ) - ); - - // Can't use `select` variable as user might provide their own and the - // reference is broken by the use of outerHTML - $('select', div) - .val( settings._iDisplayLength ) - .on( 'change.DT', function(e) { - _fnLengthChange( settings, $(this).val() ); - _fnDraw( settings ); - } ); - - // Update node value whenever anything changes the table's length - $(settings.nTable).on( 'length.dt.DT', function (e, s, len) { - if ( settings === s ) { - $('select', div).val( len ); - } - } ); - - return div[0]; - } - - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Note that most of the paging logic is done in - * DataTable.ext.pager - */ - - /** - * Generate the node required for default pagination - * @param {object} oSettings dataTables settings object - * @returns {node} Pagination feature node - * @memberof DataTable#oApi - */ - function _fnFeatureHtmlPaginate ( settings ) - { - var - type = settings.sPaginationType, - plugin = DataTable.ext.pager[ type ], - modern = typeof plugin === 'function', - redraw = function( settings ) { - _fnDraw( settings ); - }, - node = $('<div/>').addClass( settings.oClasses.sPaging + type )[0], - features = settings.aanFeatures; - - if ( ! modern ) { - plugin.fnInit( settings, node, redraw ); - } - - /* Add a draw callback for the pagination on first instance, to update the paging display */ - if ( ! features.p ) - { - node.id = settings.sTableId+'_paginate'; - - settings.aoDrawCallback.push( { - "fn": function( settings ) { - if ( modern ) { - var - start = settings._iDisplayStart, - len = settings._iDisplayLength, - visRecords = settings.fnRecordsDisplay(), - all = len === -1, - page = all ? 0 : Math.ceil( start / len ), - pages = all ? 1 : Math.ceil( visRecords / len ), - buttons = plugin(page, pages), - i, ien; - - for ( i=0, ien=features.p.length ; i<ien ; i++ ) { - _fnRenderer( settings, 'pageButton' )( - settings, features.p[i], i, buttons, page, pages - ); - } - } - else { - plugin.fnUpdate( settings, redraw ); - } - }, - "sName": "pagination" - } ); - } - - return node; - } - - /** * Alter the display settings to change the page * @param {object} settings DataTables settings object @@ -5104,7 +4786,7 @@ if ( start < 0 ) { - start = 0; + start = 0; } } else if ( action == "next" ) @@ -5118,6 +4800,10 @@ { start = Math.floor( (records-1) / len) * len; } + else if ( action === 'ellipsis' ) + { + return; + } else { _fnLog( settings, 0, "Unknown paging action: "+action, 5 ); @@ -5126,57 +4812,58 @@ var changed = settings._iDisplayStart !== start; settings._iDisplayStart = start; - if ( changed ) { - _fnCallbackFire( settings, null, 'page', [settings] ); + _fnCallbackFire( settings, null, changed ? 'page' : 'page-nc', [settings] ); - if ( redraw ) { - _fnDraw( settings ); - } - } - else { - // No change event - paging was called, but no change - _fnCallbackFire( settings, null, 'page-nc', [settings] ); + if ( changed && redraw ) { + _fnDraw( settings ); } return changed; } - /** * Generate the node required for the processing node - * @param {object} settings dataTables settings object - * @returns {node} Processing element - * @memberof DataTable#oApi + * @param {object} settings DataTables settings object */ - function _fnFeatureHtmlProcessing ( settings ) + function _processingHtml ( settings ) { - return $('<div/>', { - 'id': ! settings.aanFeatures.r ? settings.sTableId+'_processing' : null, - 'class': settings.oClasses.sProcessing, - 'role': 'status' - } ) - .html( settings.oLanguage.sProcessing ) - .append('<div><div></div><div></div><div></div><div></div></div>') - .insertBefore( settings.nTable )[0]; + var table = settings.nTable; + var scrolling = settings.oScroll.sX !== '' || settings.oScroll.sY !== ''; + + if ( settings.oFeatures.bProcessing ) { + var n = $('<div/>', { + 'id': settings.sTableId + '_processing', + 'class': settings.oClasses.processing.container, + 'role': 'status' + } ) + .html( settings.oLanguage.sProcessing ) + .append('<div><div></div><div></div><div></div><div></div></div>'); + + // Different positioning depending on if scrolling is enabled or not + if (scrolling) { + n.prependTo( $('div.dt-scroll', settings.nTableWrapper) ); + } + else { + n.insertBefore( table ); + } + + $(table).on( 'processing.dt.DT', function (e, s, show) { + n.css( 'display', show ? 'block' : 'none' ); + } ); + } } /** * Display or hide the processing indicator - * @param {object} settings dataTables settings object + * @param {object} settings DataTables settings object * @param {bool} show Show the processing indicator (true) or not (false) - * @memberof DataTable#oApi */ function _fnProcessingDisplay ( settings, show ) { - if ( settings.oFeatures.bProcessing ) { - $(settings.aanFeatures.r).css( 'display', show ? 'block' : 'none' ); - } - _fnCallbackFire( settings, null, 'processing', [settings, show] ); } - /** * Add any control elements for the table - specifically scrolling * @param {object} settings dataTables settings object @@ -5196,9 +4883,9 @@ var scrollX = scroll.sX; var scrollY = scroll.sY; - var classes = settings.oClasses; - var caption = table.children('caption'); - var captionSide = caption.length ? caption[0]._captionSide : null; + var classes = settings.oClasses.scrolling; + var caption = settings.captionNode; + var captionSide = caption ? caption._captionSide : null; var headerClone = $( table[0].cloneNode(false) ); var footerClone = $( table[0].cloneNode(false) ); var footer = table.children('tfoot'); @@ -5227,9 +4914,9 @@ * table - scroll foot table * tfoot - tfoot */ - var scroller = $( _div, { 'class': classes.sScrollWrapper } ) + var scroller = $( _div, { 'class': classes.container } ) .append( - $(_div, { 'class': classes.sScrollHead } ) + $(_div, { 'class': classes.header.self } ) .css( { overflow: 'hidden', position: 'relative', @@ -5237,7 +4924,7 @@ width: scrollX ? size(scrollX) : '100%' } ) .append( - $(_div, { 'class': classes.sScrollHeadInner } ) + $(_div, { 'class': classes.header.inner } ) .css( { 'box-sizing': 'content-box', width: scroll.sXInner || '100%' @@ -5254,7 +4941,7 @@ ) ) .append( - $(_div, { 'class': classes.sScrollBody } ) + $(_div, { 'class': classes.body } ) .css( { position: 'relative', overflow: 'auto', @@ -5265,14 +4952,14 @@ if ( footer ) { scroller.append( - $(_div, { 'class': classes.sScrollFoot } ) + $(_div, { 'class': classes.footer.self } ) .css( { overflow: 'hidden', border: 0, width: scrollX ? size(scrollX) : '100%' } ) .append( - $(_div, { 'class': classes.sScrollFootInner } ) + $(_div, { 'class': classes.footer.inner } ) .append( footerClone .removeAttr('id') @@ -5292,17 +4979,26 @@ var scrollFoot = footer ? children[2] : null; // When the body is scrolled, then we also want to scroll the headers - if ( scrollX ) { - $(scrollBody).on( 'scroll.DT', function (e) { - var scrollLeft = this.scrollLeft; + $(scrollBody).on( 'scroll.DT', function () { + var scrollLeft = this.scrollLeft; - scrollHead.scrollLeft = scrollLeft; + scrollHead.scrollLeft = scrollLeft; - if ( footer ) { - scrollFoot.scrollLeft = scrollLeft; - } - } ); - } + if ( footer ) { + scrollFoot.scrollLeft = scrollLeft; + } + } ); + + // When focus is put on the header cells, we might need to scroll the body + $('th, td', scrollHead).on('focus', function () { + var scrollLeft = scrollHead.scrollLeft; + + scrollBody.scrollLeft = scrollLeft; + + if ( footer ) { + scrollBody.scrollLeft = scrollLeft; + } + }); $(scrollBody).css('max-height', scrollY); if (! scroll.bCollapse) { @@ -5314,10 +5010,7 @@ settings.nScrollFoot = scrollFoot; // On redraw - align columns - settings.aoDrawCallback.push( { - "fn": _fnScrollDraw, - "sName": "scrolling" - } ); + settings.aoDrawCallback.push(_fnScrollDraw); return scroller[0]; } @@ -5331,8 +5024,8 @@ * Welcome to the most horrible function DataTables. The process that this * function follows is basically: * 1. Re-create the table inside the scrolling div - * 2. Take live measurements from the DOM - * 3. Apply the measurements to align the columns + * 2. Correct colgroup > col values if needed + * 3. Copy colgroup > col over to header and footer * 4. Clean up * * @param {object} settings dataTables settings object @@ -5344,43 +5037,20 @@ // to try and keep the minimised size as small as possible var scroll = settings.oScroll, - scrollX = scroll.sX, - scrollXInner = scroll.sXInner, - scrollY = scroll.sY, barWidth = scroll.iBarWidth, divHeader = $(settings.nScrollHead), - divHeaderStyle = divHeader[0].style, divHeaderInner = divHeader.children('div'), - divHeaderInnerStyle = divHeaderInner[0].style, divHeaderTable = divHeaderInner.children('table'), divBodyEl = settings.nScrollBody, divBody = $(divBodyEl), - divBodyStyle = divBodyEl.style, divFooter = $(settings.nScrollFoot), divFooterInner = divFooter.children('div'), divFooterTable = divFooterInner.children('table'), header = $(settings.nTHead), table = $(settings.nTable), - tableEl = table[0], - tableStyle = tableEl.style, - footer = settings.nTFoot ? $(settings.nTFoot) : null, + footer = settings.nTFoot && $('th, td', settings.nTFoot).length ? $(settings.nTFoot) : null, browser = settings.oBrowser, - ie67 = browser.bScrollOversize, - dtHeaderCells = _pluck( settings.aoColumns, 'nTh' ), - headerTrgEls, footerTrgEls, - headerSrcEls, footerSrcEls, - headerCopy, footerCopy, - headerWidths=[], footerWidths=[], - headerContent=[], footerContent=[], - idx, correction, sanityWidth, - zeroOut = function(nSizer) { - var style = nSizer.style; - style.paddingTop = "0"; - style.paddingBottom = "0"; - style.borderTopWidth = "0"; - style.borderBottomWidth = "0"; - style.height = 0; - }; + headerCopy, footerCopy; // If the scrollbar visibility has changed from the last draw, we need to // adjust the column sizes as the table width will have changed to account @@ -5396,214 +5066,102 @@ settings.scrollBarVis = scrollBarVis; } - /* - * 1. Re-create the table inside the scrolling div - */ - + // 1. Re-create the table inside the scrolling div // Remove the old minimised thead and tfoot elements in the inner table table.children('thead, tfoot').remove(); - if ( footer ) { - footerCopy = footer.clone().prependTo( table ); - footerTrgEls = footer.find('tr'); // the original tfoot is in its own table and must be sized - footerSrcEls = footerCopy.find('tr'); - footerCopy.find('[id]').removeAttr('id'); - } - // Clone the current header and footer elements and then place it into the inner table headerCopy = header.clone().prependTo( table ); - headerTrgEls = header.find('tr'); // original header is in its own table - headerSrcEls = headerCopy.find('tr'); headerCopy.find('th, td').removeAttr('tabindex'); headerCopy.find('[id]').removeAttr('id'); - - /* - * 2. Take live measurements from the DOM - do not alter the DOM itself! - */ - - // Remove old sizing and apply the calculated column widths - // Get the unique column headers in the newly created (cloned) header. We want to apply the - // calculated sizes to this header - if ( ! scrollX ) - { - divBodyStyle.width = '100%'; - divHeader[0].style.width = '100%'; - } - - $.each( _fnGetUniqueThs( settings, headerCopy ), function ( i, el ) { - idx = _fnVisibleToColumnIndex( settings, i ); - el.style.width = settings.aoColumns[idx].sWidth; - } ); - if ( footer ) { - _fnApplyToChildren( function(n) { - n.style.width = ""; - }, footerSrcEls ); - } - - // Size the table as a whole - sanityWidth = table.outerWidth(); - if ( scrollX === "" ) { - // No x scrolling - tableStyle.width = "100%"; - - // IE7 will make the width of the table when 100% include the scrollbar - // - which is shouldn't. When there is a scrollbar we need to take this - // into account. - if ( ie67 && (table.find('tbody').height() > divBodyEl.offsetHeight || - divBody.css('overflow-y') == "scroll") - ) { - tableStyle.width = _fnStringToCss( table.outerWidth() - barWidth); - } - - // Recalculate the sanity width - sanityWidth = table.outerWidth(); - } - else if ( scrollXInner !== "" ) { - // legacy x scroll inner has been given - use it - tableStyle.width = _fnStringToCss(scrollXInner); - - // Recalculate the sanity width - sanityWidth = table.outerWidth(); + footerCopy = footer.clone().prependTo( table ); + footerCopy.find('[id]').removeAttr('id'); } - // Hidden header should have zero height, so remove padding and borders. Then - // set the width based on the real headers - - // Apply all styles in one pass - _fnApplyToChildren( zeroOut, headerSrcEls ); - - // Read all widths in next pass - _fnApplyToChildren( function(nSizer) { - var style = window.getComputedStyle ? - window.getComputedStyle(nSizer).width : - _fnStringToCss( $(nSizer).width() ); - - headerContent.push( nSizer.innerHTML ); - headerWidths.push( style ); - }, headerSrcEls ); + // 2. Correct colgroup > col values if needed + // It is possible that the cell sizes are smaller than the content, so we need to + // correct colgroup>col for such cases. This can happen if the auto width detection + // uses a cell which has a longer string, but isn't the widest! For example + // "Chief Executive Officer (CEO)" is the longest string in the demo, but + // "Systems Administrator" is actually the widest string since it doesn't collapse. + // Note the use of translating into a column index to get the `col` element. This + // is because of Responsive which might remove `col` elements, knocking the alignment + // of the indexes out. + if (settings.aiDisplay.length) { + // Get the column sizes from the first row in the table + var colSizes = table.children('tbody').eq(0).children('tr').eq(0).children('th, td').map(function (vis) { + return { + idx: _fnVisibleToColumnIndex(settings, vis), + width: $(this).outerWidth() + } + }); - // Apply all widths in final pass - _fnApplyToChildren( function(nToSize, i) { - nToSize.style.width = headerWidths[i]; - }, headerTrgEls ); + // Check against what the colgroup > col is set to and correct if needed + for (var i=0 ; i<colSizes.length ; i++) { + var colEl = settings.aoColumns[ colSizes[i].idx ].colEl[0]; + var colWidth = colEl.style.width.replace('px', ''); - $(headerSrcEls).css('height', 0); + if (colWidth !== colSizes[i].width) { + colEl.style.width = colSizes[i].width + 'px'; + } + } + } - /* Same again with the footer if we have one */ - if ( footer ) - { - _fnApplyToChildren( zeroOut, footerSrcEls ); + // 3. Copy the colgroup over to the header and footer + divHeaderTable + .find('colgroup') + .remove(); - _fnApplyToChildren( function(nSizer) { - footerContent.push( nSizer.innerHTML ); - footerWidths.push( _fnStringToCss( $(nSizer).css('width') ) ); - }, footerSrcEls ); + divHeaderTable.append(settings.colgroup.clone()); - _fnApplyToChildren( function(nToSize, i) { - nToSize.style.width = footerWidths[i]; - }, footerTrgEls ); + if ( footer ) { + divFooterTable + .find('colgroup') + .remove(); - $(footerSrcEls).height(0); + divFooterTable.append(settings.colgroup.clone()); } - - /* - * 3. Apply the measurements - */ - // "Hide" the header and footer that we used for the sizing. We need to keep // the content of the cell so that the width applied to the header and body - // both match, but we want to hide it completely. We want to also fix their - // width to what they currently are - _fnApplyToChildren( function(nSizer, i) { - nSizer.innerHTML = '<div class="dataTables_sizing">'+headerContent[i]+'</div>'; - nSizer.childNodes[0].style.height = "0"; - nSizer.childNodes[0].style.overflow = "hidden"; - nSizer.style.width = headerWidths[i]; - }, headerSrcEls ); - - if ( footer ) - { - _fnApplyToChildren( function(nSizer, i) { - nSizer.innerHTML = '<div class="dataTables_sizing">'+footerContent[i]+'</div>'; - nSizer.childNodes[0].style.height = "0"; - nSizer.childNodes[0].style.overflow = "hidden"; - nSizer.style.width = footerWidths[i]; - }, footerSrcEls ); - } - - // Sanity check that the table is of a sensible width. If not then we are going to get - // misalignment - try to prevent this by not allowing the table to shrink below its min width - if ( Math.round(table.outerWidth()) < Math.round(sanityWidth) ) - { - // The min width depends upon if we have a vertical scrollbar visible or not */ - correction = ((divBodyEl.scrollHeight > divBodyEl.offsetHeight || - divBody.css('overflow-y') == "scroll")) ? - sanityWidth+barWidth : - sanityWidth; - - // IE6/7 are a law unto themselves... - if ( ie67 && (divBodyEl.scrollHeight > - divBodyEl.offsetHeight || divBody.css('overflow-y') == "scroll") - ) { - tableStyle.width = _fnStringToCss( correction-barWidth ); - } - - // And give the user a warning that we've stopped the table getting too small - if ( scrollX === "" || scrollXInner !== "" ) { - _fnLog( settings, 1, 'Possible column misalignment', 6 ); - } - } - else - { - correction = '100%'; - } - - // Apply to the container elements - divBodyStyle.width = _fnStringToCss( correction ); - divHeaderStyle.width = _fnStringToCss( correction ); + // both match, but we want to hide it completely. + $('th, td', headerCopy).each(function () { + $(this.childNodes).wrapAll('<div class="dt-scroll-sizing">'); + }); if ( footer ) { - settings.nScrollFoot.style.width = _fnStringToCss( correction ); - } - - - /* - * 4. Clean up - */ - if ( ! scrollY ) { - /* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting - * the scrollbar height from the visible display, rather than adding it on. We need to - * set the height in order to sort this. Don't want to do it in any other browsers. - */ - if ( ie67 ) { - divBodyStyle.height = _fnStringToCss( tableEl.offsetHeight+barWidth ); - } + $('th, td', footerCopy).each(function () { + $(this.childNodes).wrapAll('<div class="dt-scroll-sizing">'); + }); } - /* Finally set the width's of the header and footer tables */ - var iOuterWidth = table.outerWidth(); - divHeaderTable[0].style.width = _fnStringToCss( iOuterWidth ); - divHeaderInnerStyle.width = _fnStringToCss( iOuterWidth ); - + // 4. Clean up // Figure out if there are scrollbar present - if so then we need a the header and footer to // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar) - var bScrolling = table.height() > divBodyEl.clientHeight || divBody.css('overflow-y') == "scroll"; - var padding = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' ); - divHeaderInnerStyle[ padding ] = bScrolling ? barWidth+"px" : "0px"; + var isScrolling = Math.floor(table.height()) > divBodyEl.clientHeight || divBody.css('overflow-y') == "scroll"; + var paddingSide = 'padding' + (browser.bScrollbarLeft ? 'Left' : 'Right' ); + + // Set the width's of the header and footer tables + var outerWidth = table.outerWidth(); + + divHeaderTable.css('width', _fnStringToCss( outerWidth )); + divHeaderInner + .css('width', _fnStringToCss( outerWidth )) + .css(paddingSide, isScrolling ? barWidth+"px" : "0px"); if ( footer ) { - divFooterTable[0].style.width = _fnStringToCss( iOuterWidth ); - divFooterInner[0].style.width = _fnStringToCss( iOuterWidth ); - divFooterInner[0].style[padding] = bScrolling ? barWidth+"px" : "0px"; + divFooterTable.css('width', _fnStringToCss( outerWidth )); + divFooterInner + .css('width', _fnStringToCss( outerWidth )) + .css(paddingSide, isScrolling ? barWidth+"px" : "0px"); } // Correct DOM ordering for colgroup - comes before the thead - table.children('colgroup').insertBefore( table.children('thead') ); + table.children('colgroup').prependTo(table); - /* Adjust the position of the header in case we loose the y-scrollbar */ + // Adjust the position of the header in case we loose the y-scrollbar divBody.trigger('scroll'); // If sorting or filtering has occurred, jump the scrolling back to the top @@ -5613,148 +5171,81 @@ } } - - /** - * Apply a given function to the display child nodes of an element array (typically - * TD children of TR rows - * @param {function} fn Method to apply to the objects - * @param array {nodes} an1 List of elements to look through for display children - * @param array {nodes} an2 Another list (identical structure to the first) - optional + * Calculate the width of columns for the table + * @param {object} settings dataTables settings object * @memberof DataTable#oApi */ - function _fnApplyToChildren( fn, an1, an2 ) + function _fnCalculateColumnWidths ( settings ) { - var index=0, i=0, iLen=an1.length; - var nNode1, nNode2; - - while ( i < iLen ) { - nNode1 = an1[i].firstChild; - nNode2 = an2 ? an2[i].firstChild : null; - - while ( nNode1 ) { - if ( nNode1.nodeType === 1 ) { - if ( an2 ) { - fn( nNode1, nNode2, index ); - } - else { - fn( nNode1, index ); - } - - index++; - } - - nNode1 = nNode1.nextSibling; - nNode2 = an2 ? nNode2.nextSibling : null; - } - - i++; + // Not interested in doing column width calculation if auto-width is disabled + if (! settings.oFeatures.bAutoWidth) { + return; } - } - - - var __re_html_remove = /<.*?>/g; - - - /** - * Calculate the width of columns for the table - * @param {object} oSettings dataTables settings object - * @memberof DataTable#oApi - */ - function _fnCalculateColumnWidths ( oSettings ) - { var - table = oSettings.nTable, - columns = oSettings.aoColumns, - scroll = oSettings.oScroll, + table = settings.nTable, + columns = settings.aoColumns, + scroll = settings.oScroll, scrollY = scroll.sY, scrollX = scroll.sX, scrollXInner = scroll.sXInner, - columnCount = columns.length, - visibleColumns = _fnGetColumns( oSettings, 'bVisible' ), - headerCells = $('th', oSettings.nTHead), + visibleColumns = _fnGetColumns( settings, 'bVisible' ), tableWidthAttr = table.getAttribute('width'), // from DOM element tableContainer = table.parentNode, - userInputs = false, - i, column, columnIdx, width, outerWidth, - browser = oSettings.oBrowser, - ie67 = browser.bScrollOversize; + i, column, columnIdx; var styleWidth = table.style.width; if ( styleWidth && styleWidth.indexOf('%') !== -1 ) { tableWidthAttr = styleWidth; } - /* Convert any user input sizes into pixel sizes */ - for ( i=0 ; i<visibleColumns.length ; i++ ) { - column = columns[ visibleColumns[i] ]; + // Let plug-ins know that we are doing a recalc, in case they have changed any of the + // visible columns their own way (e.g. Responsive uses display:none). + _fnCallbackFire( + settings, + null, + 'column-calc', + {visible: visibleColumns}, + false + ); - if ( column.sWidth !== null ) { - column.sWidth = _fnConvertToWidth( column.sWidthOrig, tableContainer ); + // Construct a single row, worst case, table with the widest + // node in the data, assign any user defined widths, then insert it into + // the DOM and allow the browser to do all the hard work of calculating + // table widths + var tmpTable = $(table.cloneNode()) + .css( 'visibility', 'hidden' ) + .removeAttr( 'id' ); - userInputs = true; - } - } + // Clean up the table body + tmpTable.append('<tbody>') + var tr = $('<tr/>').appendTo( tmpTable.find('tbody') ); - /* If the number of columns in the DOM equals the number that we have to - * process in DataTables, then we can use the offsets that are created by - * the web- browser. No custom sizes can be set in order for this to happen, - * nor scrolling used - */ - if ( ie67 || ! userInputs && ! scrollX && ! scrollY && - columnCount == _fnVisbleColumns( oSettings ) && - columnCount == headerCells.length - ) { - for ( i=0 ; i<columnCount ; i++ ) { - var colIdx = _fnVisibleToColumnIndex( oSettings, i ); + // Clone the table header and footer - we can't use the header / footer + // from the cloned table, since if scrolling is active, the table's + // real header and footer are contained in different table tags + tmpTable + .append( $(settings.nTHead).clone() ) + .append( $(settings.nTFoot).clone() ); - if ( colIdx !== null ) { - columns[ colIdx ].sWidth = _fnStringToCss( headerCells.eq(i).width() ); - } - } - } - else - { - // Otherwise construct a single row, worst case, table with the widest - // node in the data, assign any user defined widths, then insert it into - // the DOM and allow the browser to do all the hard work of calculating - // table widths - var tmpTable = $(table).clone() // don't use cloneNode - IE8 will remove events on the main table - .css( 'visibility', 'hidden' ) - .removeAttr( 'id' ); - - // Clean up the table body - tmpTable.find('tbody tr').remove(); - var tr = $('<tr/>').appendTo( tmpTable.find('tbody') ); - - // Clone the table header and footer - we can't use the header / footer - // from the cloned table, since if scrolling is active, the table's - // real header and footer are contained in different table tags - tmpTable.find('thead, tfoot').remove(); - tmpTable - .append( $(oSettings.nTHead).clone() ) - .append( $(oSettings.nTFoot).clone() ); - - // Remove any assigned widths from the footer (from scrolling) - tmpTable.find('tfoot th, tfoot td').css('width', ''); - - // Apply custom sizing to the cloned header - headerCells = _fnGetUniqueThs( oSettings, tmpTable.find('thead')[0] ); - - for ( i=0 ; i<visibleColumns.length ; i++ ) { - column = columns[ visibleColumns[i] ]; - - headerCells[i].style.width = column.sWidthOrig !== null && column.sWidthOrig !== '' ? - _fnStringToCss( column.sWidthOrig ) : - ''; + // Remove any assigned widths from the footer (from scrolling) + tmpTable.find('tfoot th, tfoot td').css('width', ''); + + // Apply custom sizing to the cloned header + tmpTable.find('thead th, thead td').each( function () { + // Get the `width` from the header layout + var width = _fnColumnsSumWidth( settings, this, true, false ); + + if ( width ) { + this.style.width = width; // For scrollX we need to force the column width otherwise the // browser will collapse it. If this width is smaller than the // width the column requires, then it will have no effect - if ( column.sWidthOrig && scrollX ) { - $( headerCells[i] ).append( $('<div/>').css( { - width: column.sWidthOrig, + if ( scrollX ) { + $( this ).append( $('<div/>').css( { + width: width, margin: 0, padding: 0, border: 0, @@ -5762,96 +5253,96 @@ } ) ); } } - - // Find the widest cell for each column and put it into the table - if ( oSettings.aoData.length ) { - for ( i=0 ; i<visibleColumns.length ; i++ ) { - columnIdx = visibleColumns[i]; - column = columns[ columnIdx ]; - - $( _fnGetWidestNode( oSettings, columnIdx ) ) - .clone( false ) - .append( column.sContentPadding ) - .appendTo( tr ); - } - } - - // Tidy the temporary table - remove name attributes so there aren't - // duplicated in the dom (radio elements for example) - $('[name]', tmpTable).removeAttr('name'); - - // Table has been built, attach to the document so we can work with it. - // A holding element is used, positioned at the top of the container - // with minimal height, so it has no effect on if the container scrolls - // or not. Otherwise it might trigger scrolling when it actually isn't - // needed - var holder = $('<div/>').css( scrollX || scrollY ? - { - position: 'absolute', - top: 0, - left: 0, - height: 1, - right: 0, - overflow: 'hidden' - } : - {} - ) - .append( tmpTable ) - .appendTo( tableContainer ); - - // When scrolling (X or Y) we want to set the width of the table as - // appropriate. However, when not scrolling leave the table width as it - // is. This results in slightly different, but I think correct behaviour - if ( scrollX && scrollXInner ) { - tmpTable.width( scrollXInner ); + else { + this.style.width = ''; } - else if ( scrollX ) { - tmpTable.css( 'width', 'auto' ); - tmpTable.removeAttr('width'); + } ); - // If there is no width attribute or style, then allow the table to - // collapse - if ( tmpTable.width() < tableContainer.clientWidth && tableWidthAttr ) { - tmpTable.width( tableContainer.clientWidth ); - } - } - else if ( scrollY ) { + // Find the widest piece of data for each column and put it into the table + for ( i=0 ; i<visibleColumns.length ; i++ ) { + columnIdx = visibleColumns[i]; + column = columns[ columnIdx ]; + + var longest = _fnGetMaxLenString(settings, columnIdx); + var autoClass = _ext.type.className[column.sType]; + var text = longest + column.sContentPadding; + var insert = longest.indexOf('<') === -1 + ? document.createTextNode(text) + : text + + $('<td/>') + .addClass(autoClass) + .addClass(column.sClass) + .append(insert) + .appendTo(tr); + } + + // Tidy the temporary table - remove name attributes so there aren't + // duplicated in the dom (radio elements for example) + $('[name]', tmpTable).removeAttr('name'); + + // Table has been built, attach to the document so we can work with it. + // A holding element is used, positioned at the top of the container + // with minimal height, so it has no effect on if the container scrolls + // or not. Otherwise it might trigger scrolling when it actually isn't + // needed + var holder = $('<div/>').css( scrollX || scrollY ? + { + position: 'absolute', + top: 0, + left: 0, + height: 1, + right: 0, + overflow: 'hidden' + } : + {} + ) + .append( tmpTable ) + .appendTo( tableContainer ); + + // When scrolling (X or Y) we want to set the width of the table as + // appropriate. However, when not scrolling leave the table width as it + // is. This results in slightly different, but I think correct behaviour + if ( scrollX && scrollXInner ) { + tmpTable.width( scrollXInner ); + } + else if ( scrollX ) { + tmpTable.css( 'width', 'auto' ); + tmpTable.removeAttr('width'); + + // If there is no width attribute or style, then allow the table to + // collapse + if ( tmpTable.width() < tableContainer.clientWidth && tableWidthAttr ) { tmpTable.width( tableContainer.clientWidth ); } - else if ( tableWidthAttr ) { - tmpTable.width( tableWidthAttr ); - } + } + else if ( scrollY ) { + tmpTable.width( tableContainer.clientWidth ); + } + else if ( tableWidthAttr ) { + tmpTable.width( tableWidthAttr ); + } - // Get the width of each column in the constructed table - we need to - // know the inner width (so it can be assigned to the other table's - // cells) and the outer width so we can calculate the full width of the - // table. This is safe since DataTables requires a unique cell for each - // column, but if ever a header can span multiple columns, this will - // need to be modified. - var total = 0; - for ( i=0 ; i<visibleColumns.length ; i++ ) { - var cell = $(headerCells[i]); - var border = cell.outerWidth() - cell.width(); + // Get the width of each column in the constructed table + var total = 0; + var bodyCells = tmpTable.find('tbody tr').eq(0).children(); - // Use getBounding... where possible (not IE8-) because it can give - // sub-pixel accuracy, which we then want to round up! - var bounding = browser.bBounding ? - Math.ceil( headerCells[i].getBoundingClientRect().width ) : - cell.outerWidth(); + for ( i=0 ; i<visibleColumns.length ; i++ ) { + // Use getBounding for sub-pixel accuracy, which we then want to round up! + var bounding = bodyCells[i].getBoundingClientRect().width; - // Total is tracked to remove any sub-pixel errors as the outerWidth - // of the table might not equal the total given here (IE!). - total += bounding; + // Total is tracked to remove any sub-pixel errors as the outerWidth + // of the table might not equal the total given here + total += bounding; - // Width for each column to use - columns[ visibleColumns[i] ].sWidth = _fnStringToCss( bounding - border ); - } + // Width for each column to use + columns[ visibleColumns[i] ].sWidth = _fnStringToCss( bounding ); + } - table.style.width = _fnStringToCss( total ); + table.style.width = _fnStringToCss( total ); - // Finished with the table - ditch it - holder.remove(); - } + // Finished with the table - ditch it + holder.remove(); // If there is a width attr, we want to attach an event listener which // allows the table sizing to automatically adjust when the window is @@ -5861,80 +5352,19 @@ table.style.width = _fnStringToCss( tableWidthAttr ); } - if ( (tableWidthAttr || scrollX) && ! oSettings._reszEvt ) { + if ( (tableWidthAttr || scrollX) && ! settings._reszEvt ) { var bindResize = function () { - $(window).on('resize.DT-'+oSettings.sInstance, _fnThrottle( function () { - _fnAdjustColumnSizing( oSettings ); + $(window).on('resize.DT-'+settings.sInstance, DataTable.util.throttle( function () { + if (! settings.bDestroying) { + _fnAdjustColumnSizing( settings ); + } } ) ); }; - // IE6/7 will crash if we bind a resize event handler on page load. - // To be removed in 1.11 which drops IE6/7 support - if ( ie67 ) { - setTimeout( bindResize, 1000 ); - } - else { - bindResize(); - } - - oSettings._reszEvt = true; - } - } - - - /** - * Throttle the calls to a function. Arguments and context are maintained for - * the throttled function - * @param {function} fn Function to be called - * @param {int} [freq=200] call frequency in mS - * @returns {function} wrapped function - * @memberof DataTable#oApi - */ - var _fnThrottle = DataTable.util.throttle; - - - /** - * Convert a CSS unit width to pixels (e.g. 2em) - * @param {string} width width to be converted - * @param {node} parent parent to get the with for (required for relative widths) - optional - * @returns {int} width in pixels - * @memberof DataTable#oApi - */ - function _fnConvertToWidth ( width, parent ) - { - if ( ! width ) { - return 0; - } - - var n = $('<div/>') - .css( 'width', _fnStringToCss( width ) ) - .appendTo( parent || document.body ); - - var val = n[0].offsetWidth; - n.remove(); - - return val; - } - + bindResize(); - /** - * Get the widest node - * @param {object} settings dataTables settings object - * @param {int} colIdx column of interest - * @returns {node} widest table node - * @memberof DataTable#oApi - */ - function _fnGetWidestNode( settings, colIdx ) - { - var idx = _fnGetMaxLenString( settings, colIdx ); - if ( idx < 0 ) { - return null; + settings._reszEvt = true; } - - var data = settings.aoData[ idx ]; - return ! data.nTr ? // Might not have been created when deferred rendering - $('<td/>').html( _fnGetCellData( settings, idx, colIdx, 'display' ) )[0] : - data.anCells[ colIdx ]; } @@ -5942,25 +5372,45 @@ * Get the maximum strlen for each data column * @param {object} settings dataTables settings object * @param {int} colIdx column of interest - * @returns {string} max string length for each column + * @returns {string} string of the max length * @memberof DataTable#oApi */ function _fnGetMaxLenString( settings, colIdx ) { - var s, max=-1, maxIdx = -1; - - for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) { - s = _fnGetCellData( settings, i, colIdx, 'display' )+''; - s = s.replace( __re_html_remove, '' ); - s = s.replace( /&nbsp;/g, ' ' ); + var column = settings.aoColumns[colIdx]; - if ( s.length > max ) { - max = s.length; - maxIdx = i; + if (! column.maxLenString) { + var s, max='', maxLen = -1; + + for ( var i=0, ien=settings.aiDisplayMaster.length ; i<ien ; i++ ) { + var rowIdx = settings.aiDisplayMaster[i]; + var data = _fnGetRowDisplay(settings, rowIdx)[colIdx]; + + var cellString = data && typeof data === 'object' && data.nodeType + ? data.innerHTML + : data+''; + + // Remove id / name attributes from elements so they + // don't interfere with existing elements + cellString = cellString + .replace(/id=".*?"/g, '') + .replace(/name=".*?"/g, ''); + + s = _stripHtml(cellString) + .replace( /&nbsp;/g, ' ' ); + + if ( s.length > maxLen ) { + // We want the HTML in the string, but the length that + // is important is the stripped string + max = cellString; + maxLen = s.length; + } } + + column.maxLenString = max; } - return maxIdx; + return column.maxLenString; } @@ -5988,68 +5438,229 @@ s; } + /** + * Re-insert the `col` elements for current visibility + * + * @param {*} settings DT settings + */ + function _colGroup( settings ) { + var cols = settings.aoColumns; + + settings.colgroup.empty(); + + for (i=0 ; i<cols.length ; i++) { + if (cols[i].bVisible) { + settings.colgroup.append(cols[i].colEl); + } + } + } + + + function _fnSortInit( settings ) { + var target = settings.nTHead; + var headerRows = target.querySelectorAll('tr'); + var legacyTop = settings.bSortCellsTop; + var notSelector = ':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])'; + + // Legacy support for `orderCellsTop` + if (legacyTop === true) { + target = headerRows[0]; + } + else if (legacyTop === false) { + target = headerRows[ headerRows.length - 1 ]; + } + + _fnSortAttachListener( + settings, + target, + target === settings.nTHead + ? 'tr'+notSelector+' th'+notSelector+', tr'+notSelector+' td'+notSelector + : 'th'+notSelector+', td'+notSelector + ); + + // Need to resolve the user input array into our internal structure + var order = []; + _fnSortResolve( settings, order, settings.aaSorting ); + + settings.aaSorting = order; + } + + + function _fnSortAttachListener(settings, node, selector, column, callback) { + _fnBindAction( node, selector, function (e) { + var run = false; + var columns = column === undefined + ? _fnColumnsFromHeader( e.target ) + : [column]; + + if ( columns.length ) { + for ( var i=0, ien=columns.length ; i<ien ; i++ ) { + var ret = _fnSortAdd( settings, columns[i], i, e.shiftKey ); + + if (ret !== false) { + run = true; + } + + // If the first entry is no sort, then subsequent + // sort columns are ignored + if (settings.aaSorting.length === 1 && settings.aaSorting[0][1] === '') { + break; + } + } + + if (run) { + _fnProcessingDisplay( settings, true ); + + // Allow the processing display to show + setTimeout( function () { + _fnSort( settings ); + _fnSortDisplay( settings, settings.aiDisplay ); + + // Sort processing done - redraw has its own processing display + _fnProcessingDisplay( settings, false ); + + _fnReDraw( settings, false, false ); + + if (callback) { + callback(); + } + }, 0); + } + } + } ); + } + + /** + * Sort the display array to match the master's order + * @param {*} settings + */ + function _fnSortDisplay(settings, display) { + if (display.length < 2) { + return; + } + + var master = settings.aiDisplayMaster; + var masterMap = {}; + var map = {}; + var i; + + // Rather than needing an `indexOf` on master array, we can create a map + for (i=0 ; i<master.length ; i++) { + masterMap[master[i]] = i; + } + + // And then cache what would be the indexOf fom the display + for (i=0 ; i<display.length ; i++) { + map[display[i]] = masterMap[display[i]]; + } + + display.sort(function(a, b){ + // Short version of this function is simply `master.indexOf(a) - master.indexOf(b);` + return map[a] - map[b]; + }); + } + + + function _fnSortResolve (settings, nestedSort, sort) { + var push = function ( a ) { + if ($.isPlainObject(a)) { + if (a.idx !== undefined) { + // Index based ordering + nestedSort.push([a.idx, a.dir]); + } + else if (a.name) { + // Name based ordering + var cols = _pluck( settings.aoColumns, 'sName'); + var idx = cols.indexOf(a.name); + + if (idx !== -1) { + nestedSort.push([idx, a.dir]); + } + } + } + else { + // Plain column index and direction pair + nestedSort.push(a); + } + }; + + if ( $.isPlainObject(sort) ) { + // Object + push(sort); + } + else if ( sort.length && typeof sort[0] === 'number' ) { + // 1D array + push(sort); + } + else if ( sort.length ) { + // 2D array + for (var z=0; z<sort.length; z++) { + push(sort[z]); // Object or array + } + } + } function _fnSortFlatten ( settings ) { var - i, iLen, k, kLen, + i, k, kLen, aSort = [], - aiOrig = [], + extSort = DataTable.ext.type.order, aoColumns = settings.aoColumns, aDataSort, iCol, sType, srcCol, fixed = settings.aaSortingFixed, fixedObj = $.isPlainObject( fixed ), - nestedSort = [], - add = function ( a ) { - if ( a.length && ! Array.isArray( a[0] ) ) { - // 1D array - nestedSort.push( a ); - } - else { - // 2D array - $.merge( nestedSort, a ); - } - }; + nestedSort = []; + + if ( ! settings.oFeatures.bSort ) { + return aSort; + } // Build the sort array, with pre-fix and post-fix options if they have been // specified if ( Array.isArray( fixed ) ) { - add( fixed ); + _fnSortResolve( settings, nestedSort, fixed ); } if ( fixedObj && fixed.pre ) { - add( fixed.pre ); + _fnSortResolve( settings, nestedSort, fixed.pre ); } - add( settings.aaSorting ); + _fnSortResolve( settings, nestedSort, settings.aaSorting ); if (fixedObj && fixed.post ) { - add( fixed.post ); + _fnSortResolve( settings, nestedSort, fixed.post ); } for ( i=0 ; i<nestedSort.length ; i++ ) { srcCol = nestedSort[i][0]; - aDataSort = aoColumns[ srcCol ].aDataSort; - for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ ) - { - iCol = aDataSort[k]; - sType = aoColumns[ iCol ].sType || 'string'; + if ( aoColumns[ srcCol ] ) { + aDataSort = aoColumns[ srcCol ].aDataSort; - if ( nestedSort[i]._idx === undefined ) { - nestedSort[i]._idx = $.inArray( nestedSort[i][1], aoColumns[iCol].asSorting ); - } + for ( k=0, kLen=aDataSort.length ; k<kLen ; k++ ) + { + iCol = aDataSort[k]; + sType = aoColumns[ iCol ].sType || 'string'; - aSort.push( { - src: srcCol, - col: iCol, - dir: nestedSort[i][1], - index: nestedSort[i]._idx, - type: sType, - formatter: DataTable.ext.type.order[ sType+"-pre" ] - } ); + if ( nestedSort[i]._idx === undefined ) { + nestedSort[i]._idx = aoColumns[iCol].asSorting.indexOf(nestedSort[i][1]); + } + + if ( nestedSort[i][1] ) { + aSort.push( { + src: srcCol, + col: iCol, + dir: nestedSort[i][1], + index: nestedSort[i]._idx, + type: sType, + formatter: extSort[ sType+"-pre" ], + sorter: extSort[ sType+"-"+nestedSort[i][1] ] + } ); + } + } } } @@ -6060,19 +5671,14 @@ * Change the order of the table * @param {object} oSettings dataTables settings object * @memberof DataTable#oApi - * @todo This really needs split up! */ - function _fnSort ( oSettings ) + function _fnSort ( oSettings, col, dir ) { var - i, ien, iLen, j, jLen, k, kLen, - sDataType, nTh, + i, ien, iLen, aiOrig = [], - oExtSort = DataTable.ext.type.order, + extSort = DataTable.ext.type.order, aoData = oSettings.aoData, - aoColumns = oSettings.aoColumns, - aDataSort, data, iCol, sType, oSort, - formatters = 0, sortCol, displayMaster = oSettings.aiDisplayMaster, aSort; @@ -6082,16 +5688,28 @@ // data is going to be used in the table? _fnColumnTypes( oSettings ); - aSort = _fnSortFlatten( oSettings ); + // Allow a specific column to be sorted, which will _not_ alter the display + // master + if (col !== undefined) { + var srcCol = oSettings.aoColumns[col]; + aSort = [{ + src: col, + col: col, + dir: dir, + index: 0, + type: srcCol.sType, + formatter: extSort[ srcCol.sType+"-pre" ], + sorter: extSort[ srcCol.sType+"-"+dir ] + }]; + displayMaster = displayMaster.slice(); + } + else { + aSort = _fnSortFlatten( oSettings ); + } for ( i=0, ien=aSort.length ; i<ien ; i++ ) { sortCol = aSort[i]; - // Track if we can use the fast sort algorithm - if ( sortCol.formatter ) { - formatters++; - } - // Load the data needed for the sort, for each cell _fnSortData( oSettings, sortCol.col ); } @@ -6099,137 +5717,88 @@ /* No sorting required if server-side or no sorting array */ if ( _fnDataSource( oSettings ) != 'ssp' && aSort.length !== 0 ) { - // Create a value - key array of the current row positions such that we can use their - // current position during the sort, if values match, in order to perform stable sorting + // Reset the initial positions on each pass so we get a stable sort for ( i=0, iLen=displayMaster.length ; i<iLen ; i++ ) { - aiOrig[ displayMaster[i] ] = i; + aiOrig[ i ] = i; + } + + // If the first sort is desc, then reverse the array to preserve original + // order, just in reverse + if (aSort.length && aSort[0].dir === 'desc') { + aiOrig.reverse(); } /* Do the sort - here we want multi-column sorting based on a given data source (column) * and sorting function (from oSort) in a certain direction. It's reasonably complex to * follow on it's own, but this is what we want (example two column sorting): * fnLocalSorting = function(a,b){ - * var iTest; - * iTest = oSort['string-asc']('data11', 'data12'); - * if (iTest !== 0) - * return iTest; - * iTest = oSort['numeric-desc']('data21', 'data22'); - * if (iTest !== 0) - * return iTest; + * var test; + * test = oSort['string-asc']('data11', 'data12'); + * if (test !== 0) + * return test; + * test = oSort['numeric-desc']('data21', 'data22'); + * if (test !== 0) + * return test; * return oSort['numeric-asc']( aiOrig[a], aiOrig[b] ); * } * Basically we have a test for each sorting column, if the data in that column is equal, * test the next column. If all columns match, then we use a numeric sort on the row * positions in the original data array to provide a stable sort. - * - * Note - I know it seems excessive to have two sorting methods, but the first is around - * 15% faster, so the second is only maintained for backwards compatibility with sorting - * methods which do not have a pre-sort formatting function. */ - if ( formatters === aSort.length ) { - // All sort types have formatting functions - displayMaster.sort( function ( a, b ) { - var - x, y, k, test, sort, - len=aSort.length, - dataA = aoData[a]._aSortData, - dataB = aoData[b]._aSortData; + displayMaster.sort( function ( a, b ) { + var + x, y, k, test, sort, + len=aSort.length, + dataA = aoData[a]._aSortData, + dataB = aoData[b]._aSortData; - for ( k=0 ; k<len ; k++ ) { - sort = aSort[k]; + for ( k=0 ; k<len ; k++ ) { + sort = aSort[k]; - x = dataA[ sort.col ]; - y = dataB[ sort.col ]; + // Data, which may have already been through a `-pre` function + x = dataA[ sort.col ]; + y = dataB[ sort.col ]; + + if (sort.sorter) { + // If there is a custom sorter (`-asc` or `-desc`) for this + // data type, use it + test = sort.sorter(x, y); - test = x<y ? -1 : x>y ? 1 : 0; if ( test !== 0 ) { - return sort.dir === 'asc' ? test : -test; + return test; } } + else { + // Otherwise, use generic sorting + test = x<y ? -1 : x>y ? 1 : 0; - x = aiOrig[a]; - y = aiOrig[b]; - return x<y ? -1 : x>y ? 1 : 0; - } ); - } - else { - // Depreciated - remove in 1.11 (providing a plug-in option) - // Not all sort types have formatting methods, so we have to call their sorting - // methods. - displayMaster.sort( function ( a, b ) { - var - x, y, k, l, test, sort, fn, - len=aSort.length, - dataA = aoData[a]._aSortData, - dataB = aoData[b]._aSortData; - - for ( k=0 ; k<len ; k++ ) { - sort = aSort[k]; - - x = dataA[ sort.col ]; - y = dataB[ sort.col ]; - - fn = oExtSort[ sort.type+"-"+sort.dir ] || oExtSort[ "string-"+sort.dir ]; - test = fn( x, y ); if ( test !== 0 ) { - return test; + return sort.dir === 'asc' ? test : -test; } } + } - x = aiOrig[a]; - y = aiOrig[b]; - return x<y ? -1 : x>y ? 1 : 0; - } ); - } - } - - /* Tell the draw function that we have sorted the data */ - oSettings.bSorted = true; - } - - - function _fnSortAria ( settings ) - { - var label; - var nextSort; - var columns = settings.aoColumns; - var aSort = _fnSortFlatten( settings ); - var oAria = settings.oLanguage.oAria; - - // ARIA attributes - need to loop all columns, to update all (removing old - // attributes as needed) - for ( var i=0, iLen=columns.length ; i<iLen ; i++ ) - { - var col = columns[i]; - var asSorting = col.asSorting; - var sTitle = col.ariaTitle || col.sTitle.replace( /<.*?>/g, "" ); - var th = col.nTh; - - // IE7 is throwing an error when setting these properties with jQuery's - // attr() and removeAttr() methods... - th.removeAttribute('aria-sort'); + x = aiOrig[a]; + y = aiOrig[b]; - /* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */ - if ( col.bSortable ) { - if ( aSort.length > 0 && aSort[0].col == i ) { - th.setAttribute('aria-sort', aSort[0].dir=="asc" ? "ascending" : "descending" ); - nextSort = asSorting[ aSort[0].index+1 ] || asSorting[0]; - } - else { - nextSort = asSorting[0]; - } + return x<y ? -1 : x>y ? 1 : 0; + } ); + } + else if ( aSort.length === 0 ) { + // Apply index order + displayMaster.sort(function (x, y) { + return x<y ? -1 : x>y ? 1 : 0; + }); + } - label = sTitle + ( nextSort === "asc" ? - oAria.sSortAscending : - oAria.sSortDescending - ); - } - else { - label = sTitle; - } + if (col === undefined) { + // Tell the draw function that we have sorted the data + oSettings.bSorted = true; - th.setAttribute('aria-label', label); + _fnCallbackFire( oSettings, null, 'order', [oSettings, aSort] ); } + + return displayMaster; } @@ -6238,12 +5807,12 @@ * @param {object} settings dataTables settings object * @param {node} attachTo node to attach the handler to * @param {int} colIdx column sorting index - * @param {boolean} [append=false] Append the requested sort to the existing - * sort if true (i.e. multi-column sort) + * @param {int} addIndex Counter + * @param {boolean} [shift=false] Shift click add * @param {function} [callback] callback function * @memberof DataTable#oApi */ - function _fnSortListener ( settings, colIdx, append, callback ) + function _fnSortAdd ( settings, colIdx, addIndex, shift ) { var col = settings.aoColumns[ colIdx ]; var sorting = settings.aaSorting; @@ -6252,7 +5821,7 @@ var next = function ( a, overflow ) { var idx = a._idx; if ( idx === undefined ) { - idx = $.inArray( a[1], asSorting ); + idx = asSorting.indexOf(a[1]); } return idx+1 < asSorting.length ? @@ -6262,15 +5831,19 @@ 0; }; + if ( ! col.bSortable ) { + return false; + } + // Convert to 2D array if needed if ( typeof sorting[0] === 'number' ) { sorting = settings.aaSorting = [ sorting ]; } // If appending the sort then we are multi-column sorting - if ( append && settings.oFeatures.bSortMulti ) { + if ( (shift || addIndex) && settings.oFeatures.bSortMulti ) { // Are we already doing some kind of sort on this column? - var sortIdx = $.inArray( colIdx, _pluck(sorting, '0') ); + var sortIdx = _pluck(sorting, '0').indexOf(colIdx); if ( sortIdx !== -1 ) { // Yes, modify the sort @@ -6288,11 +5861,18 @@ sorting[sortIdx]._idx = nextSortIdx; } } - else { - // No sort on this column yet + else if (shift) { + // No sort on this column yet, being added by shift click + // add it as itself sorting.push( [ colIdx, asSorting[0], 0 ] ); sorting[sorting.length-1]._idx = 0; } + else { + // No sort on this column yet, being added from a colspan + // so add with same direction as first column + sorting.push( [ colIdx, sorting[0][1], 0 ] ); + sorting[sorting.length-1]._idx = 0; + } } else if ( sorting.length && sorting[0][0] == colIdx ) { // Single column - already sorting on this column, modify the sort @@ -6308,54 +5888,6 @@ sorting.push( [ colIdx, asSorting[0] ] ); sorting[0]._idx = 0; } - - // Run the sort by calling a full redraw - _fnReDraw( settings ); - - // callback used for async user interaction - if ( typeof callback == 'function' ) { - callback( settings ); - } - } - - - /** - * Attach a sort handler (click) to a node - * @param {object} settings dataTables settings object - * @param {node} attachTo node to attach the handler to - * @param {int} colIdx column sorting index - * @param {function} [callback] callback function - * @memberof DataTable#oApi - */ - function _fnSortAttachListener ( settings, attachTo, colIdx, callback ) - { - var col = settings.aoColumns[ colIdx ]; - - _fnBindAction( attachTo, {}, function (e) { - /* If the column is not sortable - don't to anything */ - if ( col.bSortable === false ) { - return; - } - - // If processing is enabled use a timeout to allow the processing - // display to be shown - otherwise to it synchronously - if ( settings.oFeatures.bProcessing ) { - _fnProcessingDisplay( settings, true ); - - setTimeout( function() { - _fnSortListener( settings, colIdx, e.shiftKey, callback ); - - // In server-side processing, the draw callback will remove the - // processing display - if ( _fnDataSource( settings ) !== 'ssp' ) { - _fnProcessingDisplay( settings, false ); - } - }, 0 ); - } - else { - _fnSortListener( settings, colIdx, e.shiftKey, callback ); - } - } ); } @@ -6368,7 +5900,7 @@ function _fnSortingClasses( settings ) { var oldSort = settings.aLastSort; - var sortClass = settings.oClasses.sSortColumn; + var sortClass = settings.oClasses.order.position; var sort = _fnSortFlatten( settings ); var features = settings.oFeatures; var i, ien, colIdx; @@ -6398,48 +5930,54 @@ // Get the data to sort a column, be it from cache, fresh (populating the // cache), or from a sort formatter - function _fnSortData( settings, idx ) + function _fnSortData( settings, colIdx ) { // Custom sorting function - provided by the sort data type - var column = settings.aoColumns[ idx ]; + var column = settings.aoColumns[ colIdx ]; var customSort = DataTable.ext.order[ column.sSortDataType ]; var customData; if ( customSort ) { - customData = customSort.call( settings.oInstance, settings, idx, - _fnColumnIndexToVisible( settings, idx ) + customData = customSort.call( settings.oInstance, settings, colIdx, + _fnColumnIndexToVisible( settings, colIdx ) ); } // Use / populate cache var row, cellData; var formatter = DataTable.ext.type.order[ column.sType+"-pre" ]; + var data = settings.aoData; + + for ( var rowIdx=0 ; rowIdx<data.length ; rowIdx++ ) { + // Sparse array + if (! data[rowIdx]) { + continue; + } - for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) { - row = settings.aoData[i]; + row = data[rowIdx]; if ( ! row._aSortData ) { row._aSortData = []; } - if ( ! row._aSortData[idx] || customSort ) { + if ( ! row._aSortData[colIdx] || customSort ) { cellData = customSort ? - customData[i] : // If there was a custom sort function, use data from there - _fnGetCellData( settings, i, idx, 'sort' ); + customData[rowIdx] : // If there was a custom sort function, use data from there + _fnGetCellData( settings, rowIdx, colIdx, 'sort' ); - row._aSortData[ idx ] = formatter ? - formatter( cellData ) : + row._aSortData[ colIdx ] = formatter ? + formatter( cellData, settings ) : cellData; } } } - /** - * Save the state of a table - * @param {object} oSettings dataTables settings object - * @memberof DataTable#oApi + * State information for a table + * + * @param {*} settings + * @returns State object */ function _fnSaveState ( settings ) { @@ -6453,11 +5991,11 @@ start: settings._iDisplayStart, length: settings._iDisplayLength, order: $.extend( true, [], settings.aaSorting ), - search: _fnSearchToCamel( settings.oPreviousSearch ), - columns: $.map( settings.aoColumns, function ( col, i ) { + search: $.extend({}, settings.oPreviousSearch), + columns: settings.aoColumns.map( function ( col, i ) { return { visible: col.bVisible, - search: _fnSearchToCamel( settings.aoPreSearchCols[i] ) + search: $.extend({}, settings.aoPreSearchCols[i]) }; } ) }; @@ -6479,7 +6017,7 @@ * @param {function} callback Callback to execute when the state has been loaded * @memberof DataTable#oApi */ - function _fnLoadState ( settings, oInit, callback ) + function _fnLoadState ( settings, init, callback ) { if ( ! settings.oFeatures.bStateSave ) { callback(); @@ -6515,18 +6053,18 @@ return; } - // Allow custom and plug-in manipulation functions to alter the saved data set and - // cancelling of loading by returning false - var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] ); - if ( $.inArray( false, abStateLoad ) !== -1 ) { + // Reject old data + var duration = settings.iStateDuration; + if ( duration > 0 && s.time < +new Date() - (duration*1000) ) { settings._bLoadingState = false; callback(); return; } - // Reject old data - var duration = settings.iStateDuration; - if ( duration > 0 && s.time < +new Date() - (duration*1000) ) { + // Allow custom and plug-in manipulation functions to alter the saved data set and + // cancelling of loading by returning false + var abStateLoad = _fnCallbackFire( settings, 'aoStateLoadParams', 'stateLoadParams', [settings, s] ); + if ( abStateLoad.indexOf(false) !== -1 ) { settings._bLoadingState = false; callback(); return; @@ -6542,6 +6080,10 @@ // Store the saved state so it might be accessed at any time settings.oLoadedState = $.extend( true, {}, s ); + // This is needed for ColReorder, which has to happen first to allow all + // the stored indexes to be usable. It is not publicly documented. + _fnCallbackFire( settings, null, 'stateLoadInit', [settings, s], true ); + // Page Length if ( s.length !== undefined ) { // If already initialised just set the value directly so that the select element is also updated @@ -6578,7 +6120,7 @@ // Search if ( s.search !== undefined ) { - $.extend( settings.oPreviousSearch, _fnSearchToHung( s.search ) ); + $.extend( settings.oPreviousSearch, s.search ); } // Columns @@ -6600,7 +6142,7 @@ // Search if ( col.search !== undefined ) { - $.extend( settings.aoPreSearchCols[i], _fnSearchToHung( col.search ) ); + $.extend( settings.aoPreSearchCols[i], col.search ); } } @@ -6613,26 +6155,8 @@ settings._bLoadingState = false; _fnCallbackFire( settings, 'aoStateLoaded', 'stateLoaded', [settings, s] ); callback(); - }; - - - /** - * Return the settings object for a particular table - * @param {node} table table we are using as a dataTable - * @returns {object} Settings object - or null if not found - * @memberof DataTable#oApi - */ - function _fnSettingsFromNode ( table ) - { - var settings = DataTable.settings; - var idx = $.inArray( table, _pluck( settings, 'nTable' ) ); - - return idx !== -1 ? - settings[ idx ] : - null; } - /** * Log an error message * @param {object} settings dataTables settings object @@ -6648,7 +6172,7 @@ if ( tn ) { msg += '. For more information about this error, please see '+ - 'http://datatables.net/tn/'+tn; + 'https://datatables.net/tn/'+tn; } if ( ! level ) { @@ -6657,7 +6181,7 @@ var type = ext.sErrMode || ext.errMode; if ( settings ) { - _fnCallbackFire( settings, null, 'error', [ settings, tn, msg ] ); + _fnCallbackFire( settings, null, 'dt-error', [ settings, tn, msg ], true ); } if ( type == 'alert' ) { @@ -6731,7 +6255,7 @@ var val; for ( var prop in extender ) { - if ( extender.hasOwnProperty(prop) ) { + if ( Object.prototype.hasOwnProperty.call(extender, prop) ) { val = extender[prop]; if ( $.isPlainObject( val ) ) { @@ -6758,47 +6282,42 @@ * This is good for accessibility since a return on the keyboard will have the * same effect as a click, if the element has focus. * @param {element} n Element to bind the action to - * @param {object} oData Data object to pass to the triggered function + * @param {object|string} selector Selector (for delegated events) or data object + * to pass to the triggered function * @param {function} fn Callback function for when the event is triggered * @memberof DataTable#oApi */ - function _fnBindAction( n, oData, fn ) + function _fnBindAction( n, selector, fn ) { $(n) - .on( 'click.DT', oData, function (e) { - $(n).trigger('blur'); // Remove focus outline for mouse users + .on( 'click.DT', selector, function (e) { + fn(e); + } ) + .on( 'keypress.DT', selector, function (e){ + if ( e.which === 13 ) { + e.preventDefault(); fn(e); - } ) - .on( 'keypress.DT', oData, function (e){ - if ( e.which === 13 ) { - e.preventDefault(); - fn(e); - } - } ) - .on( 'selectstart.DT', function () { - /* Take the brutal approach to cancelling text selection */ - return false; - } ); + } + } ) + .on( 'selectstart.DT', selector, function () { + // Don't want a double click resulting in text selection + return false; + } ); } /** * Register a callback function. Easily allows a callback function to be added to * an array store of callback functions that can then all be called together. - * @param {object} oSettings dataTables settings object - * @param {string} sStore Name of the array storage for the callbacks in oSettings + * @param {object} settings dataTables settings object + * @param {string} store Name of the array storage for the callbacks in oSettings * @param {function} fn Function to be called back - * @param {string} sName Identifying name for the callback (i.e. a label) * @memberof DataTable#oApi */ - function _fnCallbackReg( oSettings, sStore, fn, sName ) + function _fnCallbackReg( settings, store, fn ) { - if ( fn ) - { - oSettings[sStore].push( { - "fn": fn, - "sName": sName - } ); + if ( fn ) { + settings[store].push(fn); } } @@ -6815,27 +6334,31 @@ * null no trigger is fired * @param {array} args Array of arguments to pass to the callback function / * trigger + * @param {boolean} [bubbles] True if the event should bubble * @memberof DataTable#oApi */ - function _fnCallbackFire( settings, callbackArr, eventName, args ) + function _fnCallbackFire( settings, callbackArr, eventName, args, bubbles ) { var ret = []; if ( callbackArr ) { - ret = $.map( settings[callbackArr].slice().reverse(), function (val, i) { - return val.fn.apply( settings.oInstance, args ); + ret = settings[callbackArr].slice().reverse().map( function (val) { + return val.apply( settings.oInstance, args ); } ); } - if ( eventName !== null ) { + if ( eventName !== null) { var e = $.Event( eventName+'.dt' ); var table = $(settings.nTable); + + // Expose the DataTables API on the event object for easy access + e.dt = settings.api; - table.trigger( e, args ); + table[bubbles ? 'trigger' : 'triggerHandler']( e, args ); // If not yet attached to the document, trigger the event // on the body directly to sort of simulate the bubble - if (table.parents('body').length === 0) { + if (bubbles && table.parents('body').length === 0) { $('body').trigger( e, args ); } @@ -6905,12 +6428,43 @@ if ( settings.oFeatures.bServerSide ) { return 'ssp'; } - else if ( settings.ajax || settings.sAjaxSource ) { + else if ( settings.ajax ) { return 'ajax'; } return 'dom'; } + /** + * Common replacement for language strings + * + * @param {*} settings DT settings object + * @param {*} str String with values to replace + * @param {*} entries Plural number for _ENTRIES_ - can be undefined + * @returns String + */ + function _fnMacros ( settings, str, entries ) + { + // When infinite scrolling, we are always starting at 1. _iDisplayStart is + // used only internally + var + formatter = settings.fnFormatNumber, + start = settings._iDisplayStart+1, + len = settings._iDisplayLength, + vis = settings.fnRecordsDisplay(), + max = settings.fnRecordsTotal(), + all = len === -1; + + return str. + replace(/_START_/g, formatter.call( settings, start ) ). + replace(/_END_/g, formatter.call( settings, settings.fnDisplayEnd() ) ). + replace(/_MAX_/g, formatter.call( settings, max ) ). + replace(/_TOTAL_/g, formatter.call( settings, vis ) ). + replace(/_PAGE_/g, formatter.call( settings, all ? 1 : Math.ceil( start / len ) ) ). + replace(/_PAGES_/g, formatter.call( settings, all ? 1 : Math.ceil( vis / len ) ) ). + replace(/_ENTRIES_/g, settings.api.i18n('entries', '', entries) ). + replace(/_ENTRIES-MAX_/g, settings.api.i18n('entries', '', max) ). + replace(/_ENTRIES-TOTAL_/g, settings.api.i18n('entries', '', vis) ); + } @@ -6986,20 +6540,18 @@ { var idx, jq; var settings = DataTable.settings; - var tables = $.map( settings, function (el, i) { - return el.nTable; - } ); + var tables = _pluck(settings, 'nTable'); if ( ! mixed ) { return []; } - else if ( mixed.nTable && mixed.oApi ) { + else if ( mixed.nTable && mixed.oFeatures ) { // DataTables settings object return [ mixed ]; } else if ( mixed.nodeName && mixed.nodeName.toLowerCase() === 'table' ) { // Table node - idx = $.inArray( mixed, tables ); + idx = tables.indexOf(mixed); return idx !== -1 ? [ settings[idx] ] : null; } else if ( mixed && typeof mixed.settings === 'function' ) { @@ -7007,18 +6559,17 @@ } else if ( typeof mixed === 'string' ) { // jQuery selector - jq = $(mixed); + jq = $(mixed).get(); } else if ( mixed instanceof $ ) { // jQuery object (also DataTables instance) - jq = mixed; + jq = mixed.get(); } if ( jq ) { - return jq.map( function(i) { - idx = $.inArray( this, tables ); - return idx !== -1 ? settings[idx] : null; - } ).toArray(); + return settings.filter(function (v, idx) { + return jq.includes(tables[idx]); + }); } }; @@ -7075,7 +6626,7 @@ * * @example * // Initialisation as a constructor - * var api = new $.fn.DataTable.Api( 'table.dataTable' ); + * var api = new DataTable.Api( 'table.dataTable' ); */ _Api = function ( context, data ) { @@ -7101,11 +6652,13 @@ } // Remove duplicates - this.context = _unique( settings ); + this.context = settings.length > 1 + ? _unique( settings ) + : settings; // Initial data if ( data ) { - $.merge( this, data ); + this.push.apply(this, data); } // selector @@ -7128,19 +6681,13 @@ return this.count() !== 0; }, - - concat: __arrayProto.concat, - - context: [], // array of table settings objects - count: function () { return this.flatten().length; }, - each: function ( fn ) { for ( var i=0, ien=this.length ; i<ien; i++ ) { @@ -7150,7 +6697,6 @@ return this; }, - eq: function ( idx ) { var ctx = this.context; @@ -7160,47 +6706,33 @@ null; }, - filter: function ( fn ) { - var a = []; - - if ( __arrayProto.filter ) { - a = __arrayProto.filter.call( this, fn, this ); - } - else { - // Compatibility for browsers without EMCA-252-5 (JS 1.6) - for ( var i=0, ien=this.length ; i<ien ; i++ ) { - if ( fn.call( this, this[i], i, this ) ) { - a.push( this[i] ); - } - } - } + var a = __arrayProto.filter.call( this, fn, this ); return new _Api( this.context, a ); }, - flatten: function () { var a = []; + return new _Api( this.context, a.concat.apply( a, this.toArray() ) ); }, + get: function ( idx ) + { + return this[ idx ]; + }, join: __arrayProto.join, - - indexOf: __arrayProto.indexOf || function (obj, start) - { - for ( var i=(start || 0), ien=this.length ; i<ien ; i++ ) { - if ( this[i] === obj ) { - return i; - } - } - return -1; + includes: function ( find ) { + return this.indexOf( find ) === -1 ? false : true; }, + indexOf: __arrayProto.indexOf, + iterator: function ( flatten, type, fn, alwaysNew ) { var a = [], ret, @@ -7235,7 +6767,7 @@ a.push( ret ); } } - else if ( type === 'column' || type === 'column-rows' || type === 'row' || type === 'cell' ) { + else if ( type === 'every' || type === 'column' || type === 'column-rows' || type === 'row' || type === 'cell' ) { // columns and rows share the same structure. // 'this' is an array of column indexes for each context items = this[i]; @@ -7272,35 +6804,17 @@ return this; }, - - lastIndexOf: __arrayProto.lastIndexOf || function (obj, start) - { - // Bit cheeky... - return this.indexOf.apply( this.toArray.reverse(), arguments ); - }, - + lastIndexOf: __arrayProto.lastIndexOf, length: 0, - map: function ( fn ) { - var a = []; - - if ( __arrayProto.map ) { - a = __arrayProto.map.call( this, fn, this ); - } - else { - // Compatibility for browsers without EMCA-252-5 (JS 1.6) - for ( var i=0, ien=this.length ; i<ien ; i++ ) { - a.push( fn.call( this, this[i], i ) ); - } - } + var a = __arrayProto.map.call( this, fn, this ); return new _Api( this.context, a ); }, - pluck: function ( prop ) { var fn = DataTable.util.get(prop); @@ -7312,72 +6826,72 @@ pop: __arrayProto.pop, - push: __arrayProto.push, + reduce: __arrayProto.reduce, - // Does not return an API instance - reduce: __arrayProto.reduce || function ( fn, init ) - { - return _fnReduce( this, fn, init, 0, this.length, 1 ); - }, - - - reduceRight: __arrayProto.reduceRight || function ( fn, init ) - { - return _fnReduce( this, fn, init, this.length-1, -1, -1 ); - }, - + reduceRight: __arrayProto.reduceRight, reverse: __arrayProto.reverse, - // Object with rows, columns and opts selector: null, - shift: __arrayProto.shift, - slice: function () { return new _Api( this.context, this ); }, - - sort: __arrayProto.sort, // ? name - order? - + sort: __arrayProto.sort, splice: __arrayProto.splice, - toArray: function () { return __arrayProto.slice.call( this ); }, - to$: function () { return $( this ); }, - toJQuery: function () { return $( this ); }, - unique: function () { - return new _Api( this.context, _unique(this) ); + return new _Api( this.context, _unique(this.toArray()) ); }, - unshift: __arrayProto.unshift } ); + function _api_scope( scope, fn, struc ) { + return function () { + var ret = fn.apply( scope || this, arguments ); + + // Method extension + _Api.extend( ret, ret, struc.methodExt ); + return ret; + }; + } + + function _api_find( src, name ) { + for ( var i=0, ien=src.length ; i<ien ; i++ ) { + if ( src[i].name === name ) { + return src[i]; + } + } + return null; + } + + window.__apiStruct = __apiStruct; + _Api.extend = function ( scope, obj, ext ) { // Only extend API instances and static properties of the API @@ -7387,23 +6901,18 @@ var i, ien, - struct, - methodScoping = function ( scope, fn, struc ) { - return function () { - var ret = fn.apply( scope, arguments ); - - // Method extension - _Api.extend( ret, ret, struc.methodExt ); - return ret; - }; - }; + struct; for ( i=0, ien=ext.length ; i<ien ; i++ ) { struct = ext[i]; + if (struct.name === '__proto__') { + continue; + } + // Value obj[ struct.name ] = struct.type === 'function' ? - methodScoping( scope, struct.val, struct ) : + _api_scope( scope, struct.val, struct ) : struct.type === 'object' ? {} : struct.val; @@ -7415,17 +6924,6 @@ } }; - - // @todo - Is there need for an augment function? - // _Api.augment = function ( inst, name ) - // { - // // Find src object in the structure from the name - // var parts = name.split('.'); - - // _Api.extend( inst, obj ); - // }; - - // [ // { // name: 'data' -- string - Property name @@ -7449,6 +6947,7 @@ // } // ] + _Api.register = _api_register = function ( name, val ) { if ( Array.isArray( name ) ) { @@ -7464,22 +6963,13 @@ struct = __apiStruct, key, method; - var find = function ( src, name ) { - for ( var i=0, ien=src.length ; i<ien ; i++ ) { - if ( src[i].name === name ) { - return src[i]; - } - } - return null; - }; - for ( i=0, ien=heir.length ; i<ien ; i++ ) { method = heir[i].indexOf('()') !== -1; key = method ? heir[i].replace('()', '') : heir[i]; - var src = find( struct, key ); + var src = _api_find( struct, key ); if ( ! src ) { src = { name: key, @@ -7545,9 +7035,17 @@ var __table_selector = function ( selector, a ) { if ( Array.isArray(selector) ) { - return $.map( selector, function (item) { - return __table_selector(item, a); - } ); + var result = []; + + selector.forEach(function (sel) { + var inner = __table_selector(sel, a); + + result.push.apply(result, inner); + }); + + return result.filter( function (item) { + return item; + }); } // Integer is used to pick out a table by index @@ -7556,15 +7054,15 @@ } // Perform a jQuery selector on the table nodes - var nodes = $.map( a, function (el, i) { + var nodes = a.map( function (el) { return el.nTable; } ); return $(nodes) .filter( selector ) - .map( function (i) { + .map( function () { // Need to translate back from the table node to the settings - var idx = $.inArray( this, nodes ); + var idx = nodes.indexOf(this); return a[ idx ]; } ) .toArray(); @@ -7601,41 +7099,107 @@ tables; } ); + // Common methods, combined to reduce size + [ + ['nodes', 'node', 'nTable'], + ['body', 'body', 'nTBody'], + ['header', 'header', 'nTHead'], + ['footer', 'footer', 'nTFoot'], + ].forEach(function (item) { + _api_registerPlural( + 'tables().' + item[0] + '()', + 'table().' + item[1] + '()' , + function () { + return this.iterator( 'table', function ( ctx ) { + return ctx[item[2]]; + }, 1 ); + } + ); + }); - _api_registerPlural( 'tables().nodes()', 'table().node()' , function () { - return this.iterator( 'table', function ( ctx ) { - return ctx.nTable; - }, 1 ); - } ); + // Structure methods + [ + ['header', 'aoHeader'], + ['footer', 'aoFooter'], + ].forEach(function (item) { + _api_register( 'table().' + item[0] + '.structure()' , function (selector) { + var indexes = this.columns(selector).indexes().flatten(); + var ctx = this.context[0]; + + return _fnHeaderLayout(ctx, ctx[item[1]], indexes); + } ); + }) - _api_registerPlural( 'tables().body()', 'table().body()' , function () { + _api_registerPlural( 'tables().containers()', 'table().container()' , function () { return this.iterator( 'table', function ( ctx ) { - return ctx.nTBody; + return ctx.nTableWrapper; }, 1 ); } ); + _api_register( 'tables().every()', function ( fn ) { + var that = this; - _api_registerPlural( 'tables().header()', 'table().header()' , function () { - return this.iterator( 'table', function ( ctx ) { - return ctx.nTHead; - }, 1 ); - } ); + return this.iterator('table', function (s, i) { + fn.call(that.table(i), i); + }); + }); + _api_register( 'caption()', function ( value, side ) { + var context = this.context; - _api_registerPlural( 'tables().footer()', 'table().footer()' , function () { - return this.iterator( 'table', function ( ctx ) { - return ctx.nTFoot; - }, 1 ); - } ); + // Getter - return existing node's content + if ( value === undefined ) { + var caption = context[0].captionNode; + return caption && context.length ? + caption.innerHTML : + null; + } - _api_registerPlural( 'tables().containers()', 'table().container()' , function () { return this.iterator( 'table', function ( ctx ) { - return ctx.nTableWrapper; + var table = $(ctx.nTable); + var caption = $(ctx.captionNode); + var container = $(ctx.nTableWrapper); + + // Create the node if it doesn't exist yet + if ( ! caption.length ) { + caption = $('<caption/>').html( value ); + ctx.captionNode = caption[0]; + + // If side isn't set, we need to insert into the document to let the + // CSS decide so we can read it back, otherwise there is no way to + // know if the CSS would put it top or bottom for scrolling + if (! side) { + table.prepend(caption); + + side = caption.css('caption-side'); + } + } + + caption.html( value ); + + if ( side ) { + caption.css( 'caption-side', side ); + caption[0]._captionSide = side; + } + + if (container.find('div.dataTables_scroll').length) { + var selector = (side === 'top' ? 'Head' : 'Foot'); + + container.find('div.dataTables_scroll'+ selector +' table').prepend(caption); + } + else { + table.prepend(caption); + } }, 1 ); } ); + _api_register( 'caption.node()', function () { + var ctx = this.context; + + return ctx.length ? ctx[0].captionNode : null; + } ); /** @@ -7709,7 +7273,7 @@ * * `recordsDisplay` - Data set length once the current filtering criterion * are applied. */ - _api_register( 'page.info()', function ( action ) { + _api_register( 'page.info()', function () { if ( this.context.length === 0 ) { return undefined; } @@ -7786,7 +7350,7 @@ } // Trigger xhr - _fnBuildAjax( settings, [], function( json ) { + _fnBuildAjax( settings, {}, function( json ) { _fnClearTable( settings ); var data = _fnAjaxDataSrc( settings, json ); @@ -7795,6 +7359,7 @@ } _fnReDraw( settings, holdPosition ); + _fnInitComplete( settings ); _fnProcessingDisplay( settings, false ); } ); } @@ -7871,11 +7436,9 @@ } ctx = ctx[0]; - return ctx.ajax ? - $.isPlainObject( ctx.ajax ) ? - ctx.ajax.url : - ctx.ajax : - ctx.sAjaxSource; + return $.isPlainObject( ctx.ajax ) ? + ctx.ajax.url : + ctx.ajax; } // set @@ -7886,9 +7449,6 @@ else { settings.ajax = url; } - // No need to consider sAjaxSource here since DataTables gives priority - // to `ajax` over `sAjaxSource`. So setting `ajax` here, renders any - // value of `sAjaxSource` redundant. } ); } ); @@ -7928,13 +7488,18 @@ for ( i=0, ien=selector.length ; i<ien ; i++ ) { // Only split on simple strings - complex expressions will be jQuery selectors - a = selector[i] && selector[i].split && ! selector[i].match(/[\[\(:]/) ? + a = selector[i] && selector[i].split && ! selector[i].match(/[[(:]/) ? selector[i].split(',') : [ selector[i] ]; for ( j=0, jen=a.length ; j<jen ; j++ ) { res = selectFn( typeof a[j] === 'string' ? (a[j]).trim() : a[j] ); + // Remove empty items + res = res.filter( function (item) { + return item !== null && item !== undefined; + }); + if ( res && res.length ) { out = out.concat( res ); } @@ -7973,24 +7538,24 @@ }; - var _selector_first = function ( inst ) + // Reduce the API instance to the first item found + var _selector_first = function ( old ) { - // Reduce the API instance to the first item found - for ( var i=0, ien=inst.length ; i<ien ; i++ ) { - if ( inst[i].length > 0 ) { - // Assign the first element to the first item in the instance - // and truncate the instance and context - inst[0] = inst[i]; - inst[0].length = 1; - inst.length = 1; - inst.context = [ inst.context[i] ]; + let inst = new _Api(old.context[0]); - return inst; - } + // Use a push rather than passing to the constructor, since it will + // merge arrays down automatically, which isn't what is wanted here + if (old.length) { + inst.push( old[0] ); + } + + inst.selector = old.selector; + + // Limit to a single row / column / cell + if (inst.length && inst[0].length > 1) { + inst[0].splice(1); } - // Not found - return an empty instance - inst.length = 0; return inst; }; @@ -8016,7 +7581,8 @@ [] : _range( 0, displayMaster.length ); } - else if ( page == 'current' ) { + + if ( page == 'current' ) { // Current page implies that order=current and filter=applied, since it is // fairly senseless otherwise, regardless of what order and search actually // are @@ -8035,24 +7601,28 @@ // O(n+m) solution by creating a hash map var displayFilteredMap = {}; - for ( var i=0, ien=displayFiltered.length ; i<ien ; i++ ) { + for ( i=0, ien=displayFiltered.length ; i<ien ; i++ ) { displayFilteredMap[displayFiltered[i]] = null; } - a = $.map( displayMaster, function (el) { - return ! displayFilteredMap.hasOwnProperty(el) ? - el : - null; - } ); + displayMaster.forEach(function (item) { + if (! Object.prototype.hasOwnProperty.call(displayFilteredMap, item)) { + a.push(item); + } + }); } } else if ( order == 'index' || order == 'original' ) { for ( i=0, ien=settings.aoData.length ; i<ien ; i++ ) { + if (! settings.aoData[i]) { + continue; + } + if ( search == 'none' ) { a.push( i ); } else { // applied | removed - tmp = $.inArray( i, displayFiltered ); + tmp = displayFiltered.indexOf(i); if ((tmp === -1 && search == 'removed') || (tmp >= 0 && search == 'applied') ) @@ -8062,6 +7632,25 @@ } } } + else if ( typeof order === 'number' ) { + // Order the rows by the given column + var ordered = _fnSort(settings, order, 'asc'); + + if (search === 'none') { + a = ordered; + } + else { // applied | removed + for (i=0; i<ordered.length; i++) { + tmp = displayFiltered.indexOf(ordered[i]); + + if ((tmp === -1 && search == 'removed') || + (tmp >= 0 && search == 'applied') ) + { + a.push( ordered[i] ); + } + } + } + } return a; }; @@ -8082,7 +7671,6 @@ var rows; var run = function ( sel ) { var selInt = _intVal( sel ); - var i, ien; var aoData = settings.aoData; // Short cut - selector is a number and no options provided (default is @@ -8096,7 +7684,7 @@ rows = _selector_row_indexes( settings, opts ); } - if ( selInt !== null && $.inArray( selInt, rows ) !== -1 ) { + if ( selInt !== null && rows.indexOf(selInt) !== -1 ) { // Selector - integer return [ selInt ]; } @@ -8107,7 +7695,7 @@ // Selector - function if ( typeof sel === 'function' ) { - return $.map( rows, function (idx) { + return rows.map( function (idx) { var row = aoData[ idx ]; return sel( idx, row._aData, row.nTr ) ? idx : null; } ); @@ -8173,7 +7761,13 @@ .toArray(); }; - return _selector_run( 'row', selector, run, settings, opts ); + var matched = _selector_run( 'row', selector, run, settings, opts ); + + if (opts.order === 'current' || opts.order === 'applied') { + _fnSortDisplay(settings, matched); + } + + return matched; }; @@ -8247,38 +7841,15 @@ } ); _api_registerPlural( 'rows().remove()', 'row().remove()', function () { - var that = this; - - this.iterator( 'row', function ( settings, row, thatIdx ) { + this.iterator( 'row', function ( settings, row ) { var data = settings.aoData; var rowData = data[ row ]; - var i, ien, j, jen; - var loopRow, loopCells; - - data.splice( row, 1 ); - - // Update the cached indexes - for ( i=0, ien=data.length ; i<ien ; i++ ) { - loopRow = data[i]; - loopCells = loopRow.anCells; - - // Rows - if ( loopRow.nTr !== null ) { - loopRow.nTr._DT_RowIndex = i; - } - - // Cells - if ( loopCells !== null ) { - for ( j=0, jen=loopCells.length ; j<jen ; j++ ) { - loopCells[j]._DT_CellIndex.row = i; - } - } - } // Delete from the display arrays - _fnDeleteIndex( settings.aiDisplayMaster, row ); - _fnDeleteIndex( settings.aiDisplay, row ); - _fnDeleteIndex( that[ thatIdx ], row, false ); // maintain local indexes + var idx = settings.aiDisplayMaster.indexOf(row); + if (idx !== -1) { + settings.aiDisplayMaster.splice(idx, 1); + } // For server-side processing tables - subtract the deleted row from the count if ( settings._iRecordsDisplay > 0 ) { @@ -8293,12 +7864,8 @@ if ( id !== undefined ) { delete settings.aIds[ id ]; } - } ); - this.iterator( 'table', function ( settings ) { - for ( var i=0, ien=settings.aoData.length ; i<ien ; i++ ) { - settings.aoData[i].idx = i; - } + data[row] = null; } ); return this; @@ -8327,7 +7894,7 @@ // Return an Api.rows() extended instance, so rows().nodes() etc can be used var modRows = this.rows( -1 ); modRows.pop(); - $.merge( modRows, newRows ); + modRows.push.apply(modRows, newRows); return modRows; } ); @@ -8349,7 +7916,7 @@ if ( data === undefined ) { // Get - return ctx.length && this.length ? + return ctx.length && this.length && this[0].length ? ctx[0].aoData[ this[0] ]._aData : undefined; } @@ -8373,9 +7940,15 @@ _api_register( 'row().node()', function () { var ctx = this.context; - return ctx.length && this.length ? - ctx[0].aoData[ this[0] ].nTr || null : - null; + if (ctx.length && this.length && this[0].length) { + var row = ctx[0].aoData[ this[0] ]; + + if (row && row.nTr) { + return row.nTr; + } + } + + return null; } ); @@ -8400,42 +7973,49 @@ $(document).on('plugin-init.dt', function (e, context) { var api = new _Api( context ); - var namespace = 'on-plugin-init'; - var stateSaveParamsEvent = 'stateSaveParams.' + namespace; - var destroyEvent = 'destroy. ' + namespace; - api.on( stateSaveParamsEvent, function ( e, settings, d ) { + api.on( 'stateSaveParams.DT', function ( e, settings, d ) { // This could be more compact with the API, but it is a lot faster as a simple // internal loop var idFn = settings.rowIdFn; - var data = settings.aoData; + var rows = settings.aiDisplayMaster; var ids = []; - for (var i=0 ; i<data.length ; i++) { - if (data[i]._detailsShow) { - ids.push( '#' + idFn(data[i]._aData) ); + for (var i=0 ; i<rows.length ; i++) { + var rowIdx = rows[i]; + var data = settings.aoData[rowIdx]; + + if (data._detailsShow) { + ids.push( '#' + idFn(data._aData) ); } } d.childRows = ids; }); - api.on( destroyEvent, function () { - api.off(stateSaveParamsEvent + ' ' + destroyEvent); + // For future state loads (e.g. with StateRestore) + api.on( 'stateLoaded.DT', function (e, settings, state) { + __details_state_load( api, state ); }); - var loaded = api.state.loaded(); + // And the initial load state + __details_state_load( api, api.state.loaded() ); + }); - if ( loaded && loaded.childRows ) { + var __details_state_load = function (api, state) + { + if ( state && state.childRows ) { api - .rows( $.map(loaded.childRows, function (id){ - return id.replace(/:/g, '\\:') + .rows( state.childRows.map(function (id) { + // Escape any `:` characters from the row id. Accounts for + // already escaped characters. + return id.replace(/([^:\\]*(?:\\.[^:\\]*)*):/g, "$1\\:"); }) ) .every( function () { - _fnCallbackFire( context, null, 'requestChild', [ this ] ) + _fnCallbackFire( api.settings()[0], null, 'requestChild', [ this ] ) }); } - }); + } var __details_add = function ( ctx, row, data, klass ) { @@ -8453,15 +8033,18 @@ // If we get a TR element, then just add it directly - up to the dev // to add the correct number of columns etc if ( r.nodeName && r.nodeName.toLowerCase() === 'tr' ) { + r.setAttribute( 'data-dt-row', row.idx ); rows.push( r ); } else { // Otherwise create a row with a wrapper - var created = $('<tr><td></td></tr>').addClass( k ); + var created = $('<tr><td></td></tr>') + .attr( 'data-dt-row', row.idx ) + .addClass( k ); + $('td', created) .addClass( k ) - .html( r ) - [0].colSpan = _fnVisbleColumns( ctx ); + .html( r )[0].colSpan = _fnVisbleColumns( ctx ); rows.push( created[0] ); } @@ -8566,7 +8149,7 @@ } ); // Column visibility change - update the colspan - api.on( colvisEvent, function ( e, ctx, idx, vis ) { + api.on( colvisEvent, function ( e, ctx ) { if ( settings !== ctx ) { return; } @@ -8578,8 +8161,14 @@ for ( var i=0, ien=data.length ; i<ien ; i++ ) { row = data[i]; - if ( row._details ) { - row._details.children('td[colspan]').attr('colspan', visible ); + if ( row && row._details ) { + row._details.each(function () { + var el = $(this).children('td'); + + if (el.length == 1) { + el.attr('colspan', visible); + } + }); } } } ); @@ -8591,7 +8180,7 @@ } for ( var i=0, ien=data.length ; i<ien ; i++ ) { - if ( data[i]._details ) { + if ( data[i] && data[i]._details ) { __details_remove( api, i ); } } @@ -8613,9 +8202,9 @@ if ( data === undefined ) { // get - return ctx.length && this.length ? - ctx[0].aoData[ this[0] ]._details : - undefined; + return ctx.length && this.length && ctx[0].aoData[ this[0] ] + ? ctx[0].aoData[ this[0] ]._details + : undefined; } else if ( data === true ) { // show @@ -8637,7 +8226,7 @@ _api_register( [ _child_obj+'.show()', _child_mth+'.show()' // only when `child()` was called with parameters (without - ], function ( show ) { // it returns an object and this method is not executed) + ], function () { // it returns an object and this method is not executed) __details_display( this, true ); return this; } ); @@ -8664,7 +8253,7 @@ _api_register( _child_obj+'.isShown()', function () { var ctx = this.context; - if ( ctx.length && this.length ) { + if ( ctx.length && this.length && ctx[0].aoData[ this[0] ] ) { // _detailsShown as false or undefined will fall through to return false return ctx[0].aoData[ this[0] ]._detailsShow || false; } @@ -8687,27 +8276,40 @@ // can be an array of these items, comma separated list, or an array of comma // separated lists - var __re_column_selector = /^([^:]+):(name|visIdx|visible)$/; + var __re_column_selector = /^([^:]+)?:(name|title|visIdx|visible)$/; // r1 and r2 are redundant - but it means that the parameters match for the // iterator callback in columns().data() - var __columnData = function ( settings, column, r1, r2, rows ) { + var __columnData = function ( settings, column, r1, r2, rows, type ) { var a = []; for ( var row=0, ien=rows.length ; row<ien ; row++ ) { - a.push( _fnGetCellData( settings, rows[row], column ) ); + a.push( _fnGetCellData( settings, rows[row], column, type ) ); } return a; }; + var __column_header = function ( settings, column, row ) { + var header = settings.aoHeader; + var target = row !== undefined + ? row + : settings.bSortCellsTop // legacy support + ? 0 + : header.length - 1; + + return header[target][column].cell; + }; + var __column_selector = function ( settings, selector, opts ) { var columns = settings.aoColumns, names = _pluck( columns, 'sName' ), - nodes = _pluck( columns, 'nTh' ); - + titles = _pluck( columns, 'sTitle' ), + cells = DataTable.util.get('[].[].cell')(settings.aoHeader), + nodes = _unique( _flatten([], cells) ); + var run = function ( s ) { var selInt = _intVal( s ); @@ -8728,13 +8330,13 @@ if ( typeof s === 'function' ) { var rows = _selector_row_indexes( settings, opts ); - return $.map( columns, function (col, idx) { + return columns.map(function (col, idx) { return s( idx, __columnData( settings, idx, 0, 0, rows ), - nodes[ idx ] + __column_header( settings, idx ) ) ? idx : null; - } ); + }); } // jQuery or string selector @@ -8746,24 +8348,37 @@ switch( match[2] ) { case 'visIdx': case 'visible': - var idx = parseInt( match[1], 10 ); - // Visible index given, convert to column index - if ( idx < 0 ) { - // Counting from the right - var visColumns = $.map( columns, function (col,i) { - return col.bVisible ? i : null; - } ); - return [ visColumns[ visColumns.length + idx ] ]; + if (match[1]) { + var idx = parseInt( match[1], 10 ); + // Visible index given, convert to column index + if ( idx < 0 ) { + // Counting from the right + var visColumns = columns.map( function (col,i) { + return col.bVisible ? i : null; + } ); + return [ visColumns[ visColumns.length + idx ] ]; + } + // Counting from the left + return [ _fnVisibleToColumnIndex( settings, idx ) ]; } - // Counting from the left - return [ _fnVisibleToColumnIndex( settings, idx ) ]; + + // `:visible` on its own + return columns.map( function (col, i) { + return col.bVisible ? i : null; + } ); case 'name': // match by name. `names` is column index complete and in order - return $.map( names, function (name, i) { + return names.map( function (name, i) { return name === match[1] ? i : null; } ); + case 'title': + // match by column title + return titles.map( function (title, i) { + return title === match[1] ? i : null; + } ); + default: return []; } @@ -8778,7 +8393,7 @@ var jqResult = $( nodes ) .filter( s ) .map( function () { - return $.inArray( this, nodes ); // `nodes` is column index complete and in order + return _fnColumnsFromHeader( this ); // `nodes` is column index complete and in order } ) .toArray(); @@ -8803,7 +8418,7 @@ cols = settings.aoColumns, col = cols[ column ], data = settings.aoData, - row, cells, i, ien, tr; + cells, i, ien, tr; // Get if ( vis === undefined ) { @@ -8813,21 +8428,23 @@ // Set // No change if ( col.bVisible === vis ) { - return; + return false; } if ( vis ) { // Insert column // Need to decide if we should use appendChild or insertBefore - var insertBefore = $.inArray( true, _pluck(cols, 'bVisible'), column+1 ); + var insertBefore = _pluck(cols, 'bVisible').indexOf(true, column+1); for ( i=0, ien=data.length ; i<ien ; i++ ) { - tr = data[i].nTr; - cells = data[i].anCells; + if (data[i]) { + tr = data[i].nTr; + cells = data[i].anCells; - if ( tr ) { - // insertBefore can act like appendChild if 2nd arg is null - tr.insertBefore( cells[ column ], cells[ insertBefore ] || null ); + if ( tr ) { + // insertBefore can act like appendChild if 2nd arg is null + tr.insertBefore( cells[ column ], cells[ insertBefore ] || null ); + } } } } @@ -8838,6 +8455,10 @@ // Common actions col.bVisible = vis; + + _colGroup(settings); + + return true; }; @@ -8864,15 +8485,21 @@ return inst; } ); - _api_registerPlural( 'columns().header()', 'column().header()', function ( selector, opts ) { - return this.iterator( 'column', function ( settings, column ) { - return settings.aoColumns[column].nTh; + _api_registerPlural( 'columns().header()', 'column().header()', function ( row ) { + return this.iterator( 'column', function (settings, column) { + return __column_header(settings, column, row); }, 1 ); } ); - _api_registerPlural( 'columns().footer()', 'column().footer()', function ( selector, opts ) { + _api_registerPlural( 'columns().footer()', 'column().footer()', function ( row ) { return this.iterator( 'column', function ( settings, column ) { - return settings.aoColumns[column].nTf; + var footer = settings.aoFooter; + + if (! footer.length) { + return null; + } + + return settings.aoFooter[row !== undefined ? row : 0][column].cell; }, 1 ); } ); @@ -8880,6 +8507,12 @@ return this.iterator( 'column-rows', __columnData, 1 ); } ); + _api_registerPlural( 'columns().render()', 'column().render()', function ( type ) { + return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) { + return __columnData( settings, column, i, j, rows, type ); + }, 1 ); + } ); + _api_registerPlural( 'columns().dataSrc()', 'column().dataSrc()', function () { return this.iterator( 'column', function ( settings, column ) { return settings.aoColumns[column].mData; @@ -8894,19 +8527,63 @@ }, 1 ); } ); + _api_registerPlural( 'columns().init()', 'column().init()', function () { + return this.iterator( 'column', function ( settings, column ) { + return settings.aoColumns[column]; + }, 1 ); + } ); + _api_registerPlural( 'columns().nodes()', 'column().nodes()', function () { return this.iterator( 'column-rows', function ( settings, column, i, j, rows ) { return _pluck_order( settings.aoData, rows, 'anCells', column ) ; }, 1 ); } ); + _api_registerPlural( 'columns().titles()', 'column().title()', function (title, row) { + return this.iterator( 'column', function ( settings, column ) { + // Argument shifting + if (typeof title === 'number') { + row = title; + title = undefined; + } + + var span = $('span.dt-column-title', this.column(column).header(row)); + + if (title !== undefined) { + span.html(title); + return this; + } + + return span.html(); + }, 1 ); + } ); + + _api_registerPlural( 'columns().types()', 'column().type()', function () { + return this.iterator( 'column', function ( settings, column ) { + var type = settings.aoColumns[column].sType; + + // If the type was invalidated, then resolve it. This actually does + // all columns at the moment. Would only happen once if getting all + // column's data types. + if (! type) { + _fnColumnTypes(settings); + } + + return type; + }, 1 ); + } ); + _api_registerPlural( 'columns().visible()', 'column().visible()', function ( vis, calc ) { var that = this; + var changed = []; var ret = this.iterator( 'column', function ( settings, column ) { if ( vis === undefined ) { return settings.aoColumns[ column ].bVisible; } // else - __setColumnVis( settings, column, vis ); + + if (__setColumnVis( settings, column, vis )) { + changed.push(column); + } } ); // Group the column visibility changes @@ -8926,16 +8603,40 @@ // Second loop once the first is done for events that.iterator( 'column', function ( settings, column ) { - _fnCallbackFire( settings, null, 'column-visibility', [settings, column, vis, calc] ); + if (changed.includes(column)) { + _fnCallbackFire( settings, null, 'column-visibility', [settings, column, vis, calc] ); + } } ); - if ( calc === undefined || calc ) { + if ( changed.length && (calc === undefined || calc) ) { that.columns.adjust(); } }); } - return ret; + return ret; + } ); + + _api_registerPlural( 'columns().widths()', 'column().width()', function () { + // Injects a fake row into the table for just a moment so the widths can + // be read, regardless of colspan in the header and rows being present in + // the body + var columns = this.columns(':visible').count(); + var row = $('<tr>').html('<td>' + Array(columns).join('</td><td>') + '</td>'); + + $(this.table().body()).append(row); + + var widths = row.children().map(function () { + return $(this).outerWidth(); + }); + + row.remove(); + + return this.iterator( 'column', function ( settings, column ) { + var visIdx = _fnColumnIndexToVisible( settings, column ); + + return visIdx !== null ? widths[visIdx] : 0; + }, 1); } ); _api_registerPlural( 'columns().indexes()', 'column().index()', function ( type ) { @@ -9016,7 +8717,7 @@ // Selector - index if ( $.isPlainObject( s ) ) { // Valid cell index and its in the array of selectable rows - return s.column !== undefined && s.row !== undefined && $.inArray( s.row, rows ) !== -1 ? + return s.column !== undefined && s.row !== undefined && rows.indexOf(s.row) !== -1 ? [s] : []; } @@ -9028,7 +8729,7 @@ return { // use a new object, in case someone changes the values row: el._DT_CellIndex.row, column: el._DT_CellIndex.column - }; + }; } ) .toArray(); @@ -9233,6 +8934,7 @@ */ _api_register( 'order()', function ( order, dir ) { var ctx = this.context; + var args = Array.prototype.slice.call( arguments ); if ( order === undefined ) { // get @@ -9246,14 +8948,14 @@ // Simple column / direction passed in order = [ [ order, dir ] ]; } - else if ( order.length && ! Array.isArray( order[0] ) ) { + else if ( args.length > 1 ) { // Arguments passed in (list of 1D arrays) - order = Array.prototype.slice.call( arguments ); + order = args; } // otherwise a 2D array was passed in return this.iterator( 'table', function ( settings ) { - settings.aaSorting = order.slice(); + settings.aaSorting = Array.isArray(order) ? order.slice() : order; } ); } ); @@ -9270,7 +8972,7 @@ */ _api_register( 'order.listener()', function ( node, column, callback ) { return this.iterator( 'table', function ( settings ) { - _fnSortAttachListener( settings, node, column, callback ); + _fnSortAttachListener(settings, node, {}, column, callback); } ); } ); @@ -9300,18 +9002,45 @@ ], function ( dir ) { var that = this; - return this.iterator( 'table', function ( settings, i ) { - var sort = []; + if ( ! dir ) { + return this.iterator( 'column', function ( settings, idx ) { + var sort = _fnSortFlatten( settings ); - $.each( that[i], function (j, col) { - sort.push( [ col, dir ] ); + for ( var i=0, ien=sort.length ; i<ien ; i++ ) { + if ( sort[i].col === idx ) { + return sort[i].dir; + } + } + + return null; + }, 1 ); + } + else { + return this.iterator( 'table', function ( settings, i ) { + settings.aaSorting = that[i].map( function (col) { + return [ col, dir ]; + } ); } ); + } + } ); - settings.aaSorting = sort; - } ); + _api_registerPlural('columns().orderable()', 'column().orderable()', function ( directions ) { + return this.iterator( 'column', function ( settings, idx ) { + var col = settings.aoColumns[idx]; + + return directions ? + col.asSorting : + col.bSortable; + }, 1 ); } ); + _api_register( 'processing()', function ( show ) { + return this.iterator( 'table', function ( ctx ) { + _fnProcessingDisplay( ctx, show ); + } ); + } ); + _api_register( 'search()', function ( input, regex, smart, caseInsen ) { var ctx = this.context; @@ -9319,7 +9048,7 @@ if ( input === undefined ) { // get return ctx.length !== 0 ? - ctx[0].oPreviousSearch.sSearch : + ctx[0].oPreviousSearch.search : undefined; } @@ -9329,15 +9058,48 @@ return; } - _fnFilterComplete( settings, $.extend( {}, settings.oPreviousSearch, { - "sSearch": input+"", - "bRegex": regex === null ? false : regex, - "bSmart": smart === null ? true : smart, - "bCaseInsensitive": caseInsen === null ? true : caseInsen - } ), 1 ); + if (typeof regex === 'object') { + // New style options to pass to the search builder + _fnFilterComplete( settings, $.extend( settings.oPreviousSearch, regex, { + search: input + } ) ); + } + else { + // Compat for the old options + _fnFilterComplete( settings, $.extend( settings.oPreviousSearch, { + search: input, + regex: regex === null ? false : regex, + smart: smart === null ? true : smart, + caseInsensitive: caseInsen === null ? true : caseInsen + } ) ); + } } ); } ); + _api_register( 'search.fixed()', function ( name, search ) { + var ret = this.iterator( true, 'table', function ( settings ) { + var fixed = settings.searchFixed; + + if (! name) { + return Object.keys(fixed) + } + else if (search === undefined) { + return fixed[name]; + } + else if (search === null) { + delete fixed[name]; + } + else { + fixed[name] = search; + } + + return this; + } ); + + return name !== undefined && search === undefined + ? ret[0] + : ret; + } ); _api_registerPlural( 'columns().search()', @@ -9348,7 +9110,7 @@ if ( input === undefined ) { // get - return preSearch[ column ].sSearch; + return preSearch[ column ].search; } // set @@ -9356,26 +9118,78 @@ return; } - $.extend( preSearch[ column ], { - "sSearch": input+"", - "bRegex": regex === null ? false : regex, - "bSmart": smart === null ? true : smart, - "bCaseInsensitive": caseInsen === null ? true : caseInsen - } ); + if (typeof regex === 'object') { + // New style options to pass to the search builder + $.extend( preSearch[ column ], regex, { + search: input + } ); + } + else { + // Old style (with not all options available) + $.extend( preSearch[ column ], { + search: input, + regex: regex === null ? false : regex, + smart: smart === null ? true : smart, + caseInsensitive: caseInsen === null ? true : caseInsen + } ); + } - _fnFilterComplete( settings, settings.oPreviousSearch, 1 ); + _fnFilterComplete( settings, settings.oPreviousSearch ); } ); } ); + _api_register([ + 'columns().search.fixed()', + 'column().search.fixed()' + ], + function ( name, search ) { + var ret = this.iterator( true, 'column', function ( settings, colIdx ) { + var fixed = settings.aoColumns[colIdx].searchFixed; + + if (! name) { + return Object.keys(fixed) + } + else if (search === undefined) { + return fixed[name]; + } + else if (search === null) { + delete fixed[name]; + } + else { + fixed[name] = search; + } + + return this; + } ); + + return name !== undefined && search === undefined + ? ret[0] + : ret; + } + ); /* * State API methods */ - _api_register( 'state()', function () { - return this.context.length ? - this.context[0].oSavedState : - null; + _api_register( 'state()', function ( set, ignoreTime ) { + // getter + if ( ! set ) { + return this.context.length ? + this.context[0].oSavedState : + null; + } + + var setMutate = $.extend( true, {}, set ); + + // setter + return this.iterator( 'table', function ( settings ) { + if ( ignoreTime !== false ) { + setMutate.time = +new Date() + 100; + } + + _fnImplementState( settings, setMutate, function(){} ); + } ); } ); @@ -9400,8 +9214,6 @@ } ); } ); - - /** * Set the jQuery or window object to be used by DataTables * @@ -9454,6 +9266,8 @@ * * @param {string} version Version string to check for, in the format "X.Y.Z". * Note that the formats "X" and "X.Y" are also acceptable. + * @param {string} [version2=current DataTables version] As above, but optional. + * If not given the current DataTables version will be used. * @returns {boolean} true if this version of DataTables is greater or equal to * the required version, or false if this version of DataTales is not * suitable @@ -9463,9 +9277,11 @@ * @example * alert( $.fn.dataTable.versionCheck( '1.9.0' ) ); */ - DataTable.versionCheck = DataTable.fnVersionCheck = function( version ) + DataTable.versionCheck = function( version, version2 ) { - var aThis = DataTable.version.split('.'); + var aThis = version2 ? + version2.split('.') : + DataTable.version.split('.'); var aThat = version.split('.'); var iThis, iThat; @@ -9501,7 +9317,7 @@ * $('#example').dataTable(); * } */ - DataTable.isDataTable = DataTable.fnIsDataTable = function ( table ) + DataTable.isDataTable = function ( table ) { var t = $(table).get(0); var is = false; @@ -9539,7 +9355,7 @@ * $(table).DataTable().columns.adjust(); * } ); */ - DataTable.tables = DataTable.fnTables = function ( visible ) + DataTable.tables = function ( visible ) { var api = false; @@ -9548,11 +9364,15 @@ visible = visible.visible; } - var a = $.map( DataTable.settings, function (o) { - if ( !visible || (visible && $(o.nTable).is(':visible')) ) { + var a = DataTable.settings + .filter( function (o) { + return !visible || (visible && $(o.nTable).is(':visible')) + ? true + : false; + } ) + .map( function (o) { return o.nTable; - } - } ); + }); return api ? new _Api( a ) : @@ -9567,5428 +9387,2929 @@ * parameters. * * @param {object} src The model object which holds all parameters that can be - * mapped. - * @param {object} user The object to convert from camel case to Hungarian. - * @param {boolean} force When set to `true`, properties which already have a - * Hungarian value in the `user` object will be overwritten. Otherwise they - * won't be. - */ - DataTable.camelToHungarian = _fnCamelToHungarian; - - - - /** - * - */ - _api_register( '$()', function ( selector, opts ) { - var - rows = this.rows( opts ).nodes(), // Get all rows - jqRows = $(rows); - - return $( [].concat( - jqRows.filter( selector ).toArray(), - jqRows.find( selector ).toArray() - ) ); - } ); - - - // jQuery functions to operate on the tables - $.each( [ 'on', 'one', 'off' ], function (i, key) { - _api_register( key+'()', function ( /* event, handler */ ) { - var args = Array.prototype.slice.call(arguments); - - // Add the `dt` namespace automatically if it isn't already present - args[0] = $.map( args[0].split( /\s/ ), function ( e ) { - return ! e.match(/\.dt\b/) ? - e+'.dt' : - e; - } ).join( ' ' ); - - var inst = $( this.tables().nodes() ); - inst[key].apply( inst, args ); - return this; - } ); - } ); - - - _api_register( 'clear()', function () { - return this.iterator( 'table', function ( settings ) { - _fnClearTable( settings ); - } ); - } ); - - - _api_register( 'settings()', function () { - return new _Api( this.context, this.context ); - } ); - - - _api_register( 'init()', function () { - var ctx = this.context; - return ctx.length ? ctx[0].oInit : null; - } ); - - - _api_register( 'data()', function () { - return this.iterator( 'table', function ( settings ) { - return _pluck( settings.aoData, '_aData' ); - } ).flatten(); - } ); - - - _api_register( 'destroy()', function ( remove ) { - remove = remove || false; - - return this.iterator( 'table', function ( settings ) { - var classes = settings.oClasses; - var table = settings.nTable; - var tbody = settings.nTBody; - var thead = settings.nTHead; - var tfoot = settings.nTFoot; - var jqTable = $(table); - var jqTbody = $(tbody); - var jqWrapper = $(settings.nTableWrapper); - var rows = $.map( settings.aoData, function (r) { return r.nTr; } ); - var i, ien; - - // Flag to note that the table is currently being destroyed - no action - // should be taken - settings.bDestroying = true; - - // Fire off the destroy callbacks for plug-ins etc - _fnCallbackFire( settings, "aoDestroyCallback", "destroy", [settings] ); - - // If not being removed from the document, make all columns visible - if ( ! remove ) { - new _Api( settings ).columns().visible( true ); - } - - // Blitz all `DT` namespaced events (these are internal events, the - // lowercase, `dt` events are user subscribed and they are responsible - // for removing them - jqWrapper.off('.DT').find(':not(tbody *)').off('.DT'); - $(window).off('.DT-'+settings.sInstance); - - // When scrolling we had to break the table up - restore it - if ( table != thead.parentNode ) { - jqTable.children('thead').detach(); - jqTable.append( thead ); - } - - if ( tfoot && table != tfoot.parentNode ) { - jqTable.children('tfoot').detach(); - jqTable.append( tfoot ); - } - - settings.aaSorting = []; - settings.aaSortingFixed = []; - _fnSortingClasses( settings ); - - $( rows ).removeClass( settings.asStripeClasses.join(' ') ); - - $('th, td', thead).removeClass( classes.sSortable+' '+ - classes.sSortableAsc+' '+classes.sSortableDesc+' '+classes.sSortableNone - ); - - // Add the TR elements back into the table in their original order - jqTbody.children().detach(); - jqTbody.append( rows ); - - var orig = settings.nTableWrapper.parentNode; - - // Remove the DataTables generated nodes, events and classes - var removedMethod = remove ? 'remove' : 'detach'; - jqTable[ removedMethod ](); - jqWrapper[ removedMethod ](); - - // If we need to reattach the table to the document - if ( ! remove && orig ) { - // insertBefore acts like appendChild if !arg[1] - orig.insertBefore( table, settings.nTableReinsertBefore ); - - // Restore the width of the original table - was read from the style property, - // so we can restore directly to that - jqTable - .css( 'width', settings.sDestroyWidth ) - .removeClass( classes.sTable ); - - // If the were originally stripe classes - then we add them back here. - // Note this is not fool proof (for example if not all rows had stripe - // classes - but it's a good effort without getting carried away - ien = settings.asDestroyStripes.length; - - if ( ien ) { - jqTbody.children().each( function (i) { - $(this).addClass( settings.asDestroyStripes[i % ien] ); - } ); - } - } - - /* Remove the settings object from the settings array */ - var idx = $.inArray( settings, DataTable.settings ); - if ( idx !== -1 ) { - DataTable.settings.splice( idx, 1 ); - } - } ); - } ); - - - // Add the `every()` method for rows, columns and cells in a compact form - $.each( [ 'column', 'row', 'cell' ], function ( i, type ) { - _api_register( type+'s().every()', function ( fn ) { - var opts = this.selector.opts; - var api = this; - - return this.iterator( type, function ( settings, arg1, arg2, arg3, arg4 ) { - // Rows and columns: - // arg1 - index - // arg2 - table counter - // arg3 - loop counter - // arg4 - undefined - // Cells: - // arg1 - row index - // arg2 - column index - // arg3 - table counter - // arg4 - loop counter - fn.call( - api[ type ]( - arg1, - type==='cell' ? arg2 : opts, - type==='cell' ? opts : undefined - ), - arg1, arg2, arg3, arg4 - ); - } ); - } ); - } ); - - - // i18n method for extensions to be able to use the language object from the - // DataTable - _api_register( 'i18n()', function ( token, def, plural ) { - var ctx = this.context[0]; - var resolved = _fnGetObjectDataFn( token )( ctx.oLanguage ); - - if ( resolved === undefined ) { - resolved = def; - } - - if ( plural !== undefined && $.isPlainObject( resolved ) ) { - resolved = resolved[ plural ] !== undefined ? - resolved[ plural ] : - resolved._; - } - - return typeof resolved === 'string' - ? resolved.replace( '%d', plural ) // nb: plural might be undefined, - : resolved; - } ); - /** - * Version string for plug-ins to check compatibility. Allowed format is - * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used - * only for non-release builds. See http://semver.org/ for more information. - * @member - * @type string - * @default Version number + * mapped. + * @param {object} user The object to convert from camel case to Hungarian. + * @param {boolean} force When set to `true`, properties which already have a + * Hungarian value in the `user` object will be overwritten. Otherwise they + * won't be. */ - DataTable.version = "1.13.6"; + DataTable.camelToHungarian = _fnCamelToHungarian; + + /** - * Private data store, containing all of the settings objects that are - * created for the tables on a given page. * - * Note that the `DataTable.settings` object is aliased to - * `jQuery.fn.dataTableExt` through which it may be accessed and - * manipulated, or `jQuery.fn.dataTable.settings`. - * @member - * @type array - * @default [] - * @private */ - DataTable.settings = []; + _api_register( '$()', function ( selector, opts ) { + var + rows = this.rows( opts ).nodes(), // Get all rows + jqRows = $(rows); - /** - * Object models container, for the various models that DataTables has - * available to it. These models define the objects that are used to hold - * the active state and configuration of the table. - * @namespace - */ - DataTable.models = {}; + return $( [].concat( + jqRows.filter( selector ).toArray(), + jqRows.find( selector ).toArray() + ) ); + } ); + // jQuery functions to operate on the tables + $.each( [ 'on', 'one', 'off' ], function (i, key) { + _api_register( key+'()', function ( /* event, handler */ ) { + var args = Array.prototype.slice.call(arguments); - /** - * Template object for the way in which DataTables holds information about - * search information for the global filter and individual column filters. - * @namespace - */ - DataTable.models.oSearch = { - /** - * Flag to indicate if the filtering should be case insensitive or not - * @type boolean - * @default true - */ - "bCaseInsensitive": true, + // Add the `dt` namespace automatically if it isn't already present + args[0] = args[0].split( /\s/ ).map( function ( e ) { + return ! e.match(/\.dt\b/) ? + e+'.dt' : + e; + } ).join( ' ' ); - /** - * Applied search term - * @type string - * @default <i>Empty string</i> - */ - "sSearch": "", + var inst = $( this.tables().nodes() ); + inst[key].apply( inst, args ); + return this; + } ); + } ); - /** - * Flag to indicate if the search term should be interpreted as a - * regular expression (true) or not (false) and therefore and special - * regex characters escaped. - * @type boolean - * @default false - */ - "bRegex": false, - /** - * Flag to indicate if DataTables is to use its smart filtering or not. - * @type boolean - * @default true - */ - "bSmart": true, + _api_register( 'clear()', function () { + return this.iterator( 'table', function ( settings ) { + _fnClearTable( settings ); + } ); + } ); - /** - * Flag to indicate if DataTables should only trigger a search when - * the return key is pressed. - * @type boolean - * @default false - */ - "return": false - }; + _api_register( 'error()', function (msg) { + return this.iterator( 'table', function ( settings ) { + _fnLog( settings, 0, msg ); + } ); + } ); + _api_register( 'settings()', function () { + return new _Api( this.context, this.context ); + } ); - /** - * Template object for the way in which DataTables holds information about - * each individual row. This is the object format used for the settings - * aoData array. - * @namespace - */ - DataTable.models.oRow = { - /** - * TR element for the row - * @type node - * @default null - */ - "nTr": null, - /** - * Array of TD elements for each row. This is null until the row has been - * created. - * @type array nodes - * @default [] - */ - "anCells": null, + _api_register( 'init()', function () { + var ctx = this.context; + return ctx.length ? ctx[0].oInit : null; + } ); - /** - * Data object from the original data source for the row. This is either - * an array if using the traditional form of DataTables, or an object if - * using mData options. The exact type will depend on the passed in - * data from the data source, or will be an array if using DOM a data - * source. - * @type array|object - * @default [] - */ - "_aData": [], - /** - * Sorting data cache - this array is ostensibly the same length as the - * number of columns (although each index is generated only as it is - * needed), and holds the data that is used for sorting each column in the - * row. We do this cache generation at the start of the sort in order that - * the formatting of the sort data need be done only once for each cell - * per sort. This array should not be read from or written to by anything - * other than the master sorting methods. - * @type array - * @default null - * @private - */ - "_aSortData": null, + _api_register( 'data()', function () { + return this.iterator( 'table', function ( settings ) { + return _pluck( settings.aoData, '_aData' ); + } ).flatten(); + } ); - /** - * Per cell filtering data cache. As per the sort data cache, used to - * increase the performance of the filtering in DataTables - * @type array - * @default null - * @private - */ - "_aFilterData": null, - /** - * Filtering data cache. This is the same as the cell filtering cache, but - * in this case a string rather than an array. This is easily computed with - * a join on `_aFilterData`, but is provided as a cache so the join isn't - * needed on every search (memory traded for performance) - * @type array - * @default null - * @private - */ - "_sFilterRow": null, + _api_register( 'trigger()', function ( name, args, bubbles ) { + return this.iterator( 'table', function ( settings ) { + return _fnCallbackFire( settings, null, name, args, bubbles ); + } ).flatten(); + } ); - /** - * Cache of the class name that DataTables has applied to the row, so we - * can quickly look at this variable rather than needing to do a DOM check - * on className for the nTr property. - * @type string - * @default <i>Empty string</i> - * @private - */ - "_sRowStripe": "", - /** - * Denote if the original data source was from the DOM, or the data source - * object. This is used for invalidating data, so DataTables can - * automatically read data from the original source, unless uninstructed - * otherwise. - * @type string - * @default null - * @private - */ - "src": null, + _api_register( 'ready()', function ( fn ) { + var ctx = this.context; - /** - * Index in the aoData array. This saves an indexOf lookup when we have the - * object, but want to know the index - * @type integer - * @default -1 - * @private - */ - "idx": -1 - }; + // Get status of first table + if (! fn) { + return ctx.length + ? (ctx[0]._bInitComplete || false) + : null; + } + // Function to run either once the table becomes ready or + // immediately if it is already ready. + return this.tables().every(function () { + if (this.context[0]._bInitComplete) { + fn.call(this); + } + else { + this.on('init', function () { + fn.call(this); + }); + } + } ); + } ); - /** - * Template object for the column information object in DataTables. This object - * is held in the settings aoColumns array and contains all the information that - * DataTables needs about each individual column. - * - * Note that this object is related to {@link DataTable.defaults.column} - * but this one is the internal data store for DataTables's cache of columns. - * It should NOT be manipulated outside of DataTables. Any configuration should - * be done through the initialisation options. - * @namespace - */ - DataTable.models.oColumn = { - /** - * Column index. This could be worked out on-the-fly with $.inArray, but it - * is faster to just hold it as a variable - * @type integer - * @default null - */ - "idx": null, - /** - * A list of the columns that sorting should occur on when this column - * is sorted. That this property is an array allows multi-column sorting - * to be defined for a column (for example first name / last name columns - * would benefit from this). The values are integers pointing to the - * columns to be sorted on (typically it will be a single integer pointing - * at itself, but that doesn't need to be the case). - * @type array - */ - "aDataSort": null, + _api_register( 'destroy()', function ( remove ) { + remove = remove || false; - /** - * Define the sorting directions that are applied to the column, in sequence - * as the column is repeatedly sorted upon - i.e. the first value is used - * as the sorting direction when the column if first sorted (clicked on). - * Sort it again (click again) and it will move on to the next index. - * Repeat until loop. - * @type array - */ - "asSorting": null, + return this.iterator( 'table', function ( settings ) { + var classes = settings.oClasses; + var table = settings.nTable; + var tbody = settings.nTBody; + var thead = settings.nTHead; + var tfoot = settings.nTFoot; + var jqTable = $(table); + var jqTbody = $(tbody); + var jqWrapper = $(settings.nTableWrapper); + var rows = settings.aoData.map( function (r) { return r ? r.nTr : null; } ); + var orderClasses = classes.order; - /** - * Flag to indicate if the column is searchable, and thus should be included - * in the filtering or not. - * @type boolean - */ - "bSearchable": null, + // Flag to note that the table is currently being destroyed - no action + // should be taken + settings.bDestroying = true; - /** - * Flag to indicate if the column is sortable or not. - * @type boolean - */ - "bSortable": null, + // Fire off the destroy callbacks for plug-ins etc + _fnCallbackFire( settings, "aoDestroyCallback", "destroy", [settings], true ); - /** - * Flag to indicate if the column is currently visible in the table or not - * @type boolean - */ - "bVisible": null, + // If not being removed from the document, make all columns visible + if ( ! remove ) { + new _Api( settings ).columns().visible( true ); + } - /** - * Store for manual type assignment using the `column.type` option. This - * is held in store so we can manipulate the column's `sType` property. - * @type string - * @default null - * @private - */ - "_sManualType": null, + // Blitz all `DT` namespaced events (these are internal events, the + // lowercase, `dt` events are user subscribed and they are responsible + // for removing them + jqWrapper.off('.DT').find(':not(tbody *)').off('.DT'); + $(window).off('.DT-'+settings.sInstance); + + // When scrolling we had to break the table up - restore it + if ( table != thead.parentNode ) { + jqTable.children('thead').detach(); + jqTable.append( thead ); + } - /** - * Flag to indicate if HTML5 data attributes should be used as the data - * source for filtering or sorting. True is either are. - * @type boolean - * @default false - * @private - */ - "_bAttrSrc": false, + if ( tfoot && table != tfoot.parentNode ) { + jqTable.children('tfoot').detach(); + jqTable.append( tfoot ); + } - /** - * Developer definable function that is called whenever a cell is created (Ajax source, - * etc) or processed for input (DOM source). This can be used as a compliment to mRender - * allowing you to modify the DOM element (add background colour for example) when the - * element is available. - * @type function - * @param {element} nTd The TD node that has been created - * @param {*} sData The Data for the cell - * @param {array|object} oData The data for the whole row - * @param {int} iRow The row index for the aoData data store - * @default null - */ - "fnCreatedCell": null, + settings.colgroup.remove(); - /** - * Function to get data from a cell in a column. You should <b>never</b> - * access data directly through _aData internally in DataTables - always use - * the method attached to this property. It allows mData to function as - * required. This function is automatically assigned by the column - * initialisation method - * @type function - * @param {array|object} oData The data array/object for the array - * (i.e. aoData[]._aData) - * @param {string} sSpecific The specific data type you want to get - - * 'display', 'type' 'filter' 'sort' - * @returns {*} The data for the cell from the given row's data - * @default null - */ - "fnGetData": null, + settings.aaSorting = []; + settings.aaSortingFixed = []; + _fnSortingClasses( settings ); - /** - * Function to set data for a cell in the column. You should <b>never</b> - * set the data directly to _aData internally in DataTables - always use - * this method. It allows mData to function as required. This function - * is automatically assigned by the column initialisation method - * @type function - * @param {array|object} oData The data array/object for the array - * (i.e. aoData[]._aData) - * @param {*} sValue Value to set - * @default null - */ - "fnSetData": null, + $('th, td', thead) + .removeClass( + orderClasses.canAsc + ' ' + + orderClasses.canDesc + ' ' + + orderClasses.isAsc + ' ' + + orderClasses.isDesc + ) + .css('width', ''); - /** - * Property to read the value for the cells in the column from the data - * source array / object. If null, then the default content is used, if a - * function is given then the return from the function is used. - * @type function|int|string|null - * @default null - */ - "mData": null, + // Add the TR elements back into the table in their original order + jqTbody.children().detach(); + jqTbody.append( rows ); - /** - * Partner property to mData which is used (only when defined) to get - * the data - i.e. it is basically the same as mData, but without the - * 'set' option, and also the data fed to it is the result from mData. - * This is the rendering method to match the data method of mData. - * @type function|int|string|null - * @default null - */ - "mRender": null, + var orig = settings.nTableWrapper.parentNode; + var insertBefore = settings.nTableWrapper.nextSibling; - /** - * Unique header TH/TD element for this column - this is what the sorting - * listener is attached to (if sorting is enabled.) - * @type node - * @default null - */ - "nTh": null, + // Remove the DataTables generated nodes, events and classes + var removedMethod = remove ? 'remove' : 'detach'; + jqTable[ removedMethod ](); + jqWrapper[ removedMethod ](); - /** - * Unique footer TH/TD element for this column (if there is one). Not used - * in DataTables as such, but can be used for plug-ins to reference the - * footer for each column. - * @type node - * @default null - */ - "nTf": null, + // If we need to reattach the table to the document + if ( ! remove && orig ) { + // insertBefore acts like appendChild if !arg[1] + orig.insertBefore( table, insertBefore ); - /** - * The class to apply to all TD elements in the table's TBODY for the column - * @type string - * @default null - */ - "sClass": null, + // Restore the width of the original table - was read from the style property, + // so we can restore directly to that + jqTable + .css( 'width', settings.sDestroyWidth ) + .removeClass( classes.table ); + } - /** - * When DataTables calculates the column widths to assign to each column, - * it finds the longest string in each column and then constructs a - * temporary table and reads the widths from that. The problem with this - * is that "mmm" is much wider then "iiii", but the latter is a longer - * string - thus the calculation can go wrong (doing it properly and putting - * it into an DOM object and measuring that is horribly(!) slow). Thus as - * a "work around" we provide this option. It will append its value to the - * text that is found to be the longest string for the column - i.e. padding. - * @type string - */ - "sContentPadding": null, + /* Remove the settings object from the settings array */ + var idx = DataTable.settings.indexOf(settings); + if ( idx !== -1 ) { + DataTable.settings.splice( idx, 1 ); + } + } ); + } ); - /** - * Allows a default value to be given for a column's data, and will be used - * whenever a null data source is encountered (this can be because mData - * is set to null, or because the data source itself is null). - * @type string - * @default null - */ - "sDefaultContent": null, - /** - * Name for the column, allowing reference to the column by name as well as - * by index (needs a lookup to work by name). - * @type string - */ - "sName": null, + // Add the `every()` method for rows, columns and cells in a compact form + $.each( [ 'column', 'row', 'cell' ], function ( i, type ) { + _api_register( type+'s().every()', function ( fn ) { + var opts = this.selector.opts; + var api = this; + var inst; + var counter = 0; - /** - * Custom sorting data type - defines which of the available plug-ins in - * afnSortData the custom sorting will use - if any is defined. - * @type string - * @default std - */ - "sSortDataType": 'std', + return this.iterator( 'every', function ( settings, selectedIdx, tableIdx ) { + inst = api[ type ](selectedIdx, opts); - /** - * Class to be applied to the header element when sorting on this column - * @type string - * @default null - */ - "sSortingClass": null, + if (type === 'cell') { + fn.call(inst, inst[0][0].row, inst[0][0].column, tableIdx, counter); + } + else { + fn.call(inst, selectedIdx, tableIdx, counter); + } - /** - * Class to be applied to the header element when sorting on this column - - * when jQuery UI theming is used. - * @type string - * @default null - */ - "sSortingClassJUI": null, + counter++; + } ); + } ); + } ); - /** - * Title of the column - what is seen in the TH element (nTh). - * @type string - */ - "sTitle": null, - /** - * Column sorting and filtering type - * @type string - * @default null - */ - "sType": null, + // i18n method for extensions to be able to use the language object from the + // DataTable + _api_register( 'i18n()', function ( token, def, plural ) { + var ctx = this.context[0]; + var resolved = _fnGetObjectDataFn( token )( ctx.oLanguage ); - /** - * Width of the column - * @type string - * @default null - */ - "sWidth": null, + if ( resolved === undefined ) { + resolved = def; + } - /** - * Width of the column when it was first "encountered" - * @type string - * @default null - */ - "sWidthOrig": null - }; + if ( $.isPlainObject( resolved ) ) { + resolved = plural !== undefined && resolved[ plural ] !== undefined ? + resolved[ plural ] : + resolved._; + } + return typeof resolved === 'string' + ? resolved.replace( '%d', plural ) // nb: plural might be undefined, + : resolved; + } ); - /* - * Developer note: The properties of the object below are given in Hungarian - * notation, that was used as the interface for DataTables prior to v1.10, however - * from v1.10 onwards the primary interface is camel case. In order to avoid - * breaking backwards compatibility utterly with this change, the Hungarian - * version is still, internally the primary interface, but is is not documented - * - hence the @name tags in each doc comment. This allows a Javascript function - * to create a map from Hungarian notation to camel case (going the other direction - * would require each property to be listed, which would add around 3K to the size - * of DataTables, while this method is about a 0.5K hit). + /** + * Version string for plug-ins to check compatibility. Allowed format is + * `a.b.c-d` where: a:int, b:int, c:int, d:string(dev|beta|alpha). `d` is used + * only for non-release builds. See https://semver.org/ for more information. + * @member + * @type string + * @default Version number + */ + DataTable.version = "2.0.8"; + + /** + * Private data store, containing all of the settings objects that are + * created for the tables on a given page. * - * Ultimately this does pave the way for Hungarian notation to be dropped - * completely, but that is a massive amount of work and will break current - * installs (therefore is on-hold until v2). + * Note that the `DataTable.settings` object is aliased to + * `jQuery.fn.dataTableExt` through which it may be accessed and + * manipulated, or `jQuery.fn.dataTable.settings`. + * @member + * @type array + * @default [] + * @private */ + DataTable.settings = []; /** - * Initialisation options that can be given to DataTables at initialisation - * time. + * Object models container, for the various models that DataTables has + * available to it. These models define the objects that are used to hold + * the active state and configuration of the table. * @namespace */ - DataTable.defaults = { - /** - * An array of data to use for the table, passed in at initialisation which - * will be used in preference to any data which is already in the DOM. This is - * particularly useful for constructing tables purely in Javascript, for - * example with a custom Ajax call. - * @type array - * @default null - * - * @dtopt Option - * @name DataTable.defaults.data - * - * @example - * // Using a 2D array data source - * $(document).ready( function () { - * $('#example').dataTable( { - * "data": [ - * ['Trident', 'Internet Explorer 4.0', 'Win 95+', 4, 'X'], - * ['Trident', 'Internet Explorer 5.0', 'Win 95+', 5, 'C'], - * ], - * "columns": [ - * { "title": "Engine" }, - * { "title": "Browser" }, - * { "title": "Platform" }, - * { "title": "Version" }, - * { "title": "Grade" } - * ] - * } ); - * } ); - * - * @example - * // Using an array of objects as a data source (`data`) - * $(document).ready( function () { - * $('#example').dataTable( { - * "data": [ - * { - * "engine": "Trident", - * "browser": "Internet Explorer 4.0", - * "platform": "Win 95+", - * "version": 4, - * "grade": "X" - * }, - * { - * "engine": "Trident", - * "browser": "Internet Explorer 5.0", - * "platform": "Win 95+", - * "version": 5, - * "grade": "C" - * } - * ], - * "columns": [ - * { "title": "Engine", "data": "engine" }, - * { "title": "Browser", "data": "browser" }, - * { "title": "Platform", "data": "platform" }, - * { "title": "Version", "data": "version" }, - * { "title": "Grade", "data": "grade" } - * ] - * } ); - * } ); - */ - "aaData": null, - + DataTable.models = {}; - /** - * If ordering is enabled, then DataTables will perform a first pass sort on - * initialisation. You can define which column(s) the sort is performed - * upon, and the sorting direction, with this variable. The `sorting` array - * should contain an array for each column to be sorted initially containing - * the column's index and a direction string ('asc' or 'desc'). - * @type array - * @default [[0,'asc']] - * - * @dtopt Option - * @name DataTable.defaults.order - * - * @example - * // Sort by 3rd column first, and then 4th column - * $(document).ready( function() { - * $('#example').dataTable( { - * "order": [[2,'asc'], [3,'desc']] - * } ); - * } ); - * - * // No initial sorting - * $(document).ready( function() { - * $('#example').dataTable( { - * "order": [] - * } ); - * } ); - */ - "aaSorting": [[0,'asc']], - /** - * This parameter is basically identical to the `sorting` parameter, but - * cannot be overridden by user interaction with the table. What this means - * is that you could have a column (visible or hidden) which the sorting - * will always be forced on first - any sorting after that (from the user) - * will then be performed as required. This can be useful for grouping rows - * together. - * @type array - * @default null - * - * @dtopt Option - * @name DataTable.defaults.orderFixed - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "orderFixed": [[0,'asc']] - * } ); - * } ) + /** + * Template object for the way in which DataTables holds information about + * search information for the global filter and individual column filters. + * @namespace + */ + DataTable.models.oSearch = { + /** + * Flag to indicate if the filtering should be case insensitive or not */ - "aaSortingFixed": [], + "caseInsensitive": true, + /** + * Applied search term + */ + "search": "", /** - * DataTables can be instructed to load data to display in the table from a - * Ajax source. This option defines how that Ajax call is made and where to. - * - * The `ajax` property has three different modes of operation, depending on - * how it is defined. These are: - * - * * `string` - Set the URL from where the data should be loaded from. - * * `object` - Define properties for `jQuery.ajax`. - * * `function` - Custom data get function - * - * `string` - * -------- - * - * As a string, the `ajax` property simply defines the URL from which - * DataTables will load data. - * - * `object` - * -------- - * - * As an object, the parameters in the object are passed to - * [jQuery.ajax](http://api.jquery.com/jQuery.ajax/) allowing fine control - * of the Ajax request. DataTables has a number of default parameters which - * you can override using this option. Please refer to the jQuery - * documentation for a full description of the options available, although - * the following parameters provide additional options in DataTables or - * require special consideration: - * - * * `data` - As with jQuery, `data` can be provided as an object, but it - * can also be used as a function to manipulate the data DataTables sends - * to the server. The function takes a single parameter, an object of - * parameters with the values that DataTables has readied for sending. An - * object may be returned which will be merged into the DataTables - * defaults, or you can add the items to the object that was passed in and - * not return anything from the function. This supersedes `fnServerParams` - * from DataTables 1.9-. - * - * * `dataSrc` - By default DataTables will look for the property `data` (or - * `aaData` for compatibility with DataTables 1.9-) when obtaining data - * from an Ajax source or for server-side processing - this parameter - * allows that property to be changed. You can use Javascript dotted - * object notation to get a data source for multiple levels of nesting, or - * it my be used as a function. As a function it takes a single parameter, - * the JSON returned from the server, which can be manipulated as - * required, with the returned value being that used by DataTables as the - * data source for the table. This supersedes `sAjaxDataProp` from - * DataTables 1.9-. - * - * * `success` - Should not be overridden it is used internally in - * DataTables. To manipulate / transform the data returned by the server - * use `ajax.dataSrc`, or use `ajax` as a function (see below). - * - * `function` - * ---------- - * - * As a function, making the Ajax call is left up to yourself allowing - * complete control of the Ajax request. Indeed, if desired, a method other - * than Ajax could be used to obtain the required data, such as Web storage - * or an AIR database. - * - * The function is given four parameters and no return is required. The - * parameters are: - * - * 1. _object_ - Data to send to the server - * 2. _function_ - Callback function that must be executed when the required - * data has been obtained. That data should be passed into the callback - * as the only parameter - * 3. _object_ - DataTables settings object for the table - * - * Note that this supersedes `fnServerData` from DataTables 1.9-. - * - * @type string|object|function - * @default null - * - * @dtopt Option - * @name DataTable.defaults.ajax - * @since 1.10.0 - * - * @example - * // Get JSON data from a file via Ajax. - * // Note DataTables expects data in the form `{ data: [ ...data... ] }` by default). - * $('#example').dataTable( { - * "ajax": "data.json" - * } ); - * - * @example - * // Get JSON data from a file via Ajax, using `dataSrc` to change - * // `data` to `tableData` (i.e. `{ tableData: [ ...data... ] }`) - * $('#example').dataTable( { - * "ajax": { - * "url": "data.json", - * "dataSrc": "tableData" - * } - * } ); - * - * @example - * // Get JSON data from a file via Ajax, using `dataSrc` to read data - * // from a plain array rather than an array in an object - * $('#example').dataTable( { - * "ajax": { - * "url": "data.json", - * "dataSrc": "" - * } - * } ); - * - * @example - * // Manipulate the data returned from the server - add a link to data - * // (note this can, should, be done using `render` for the column - this - * // is just a simple example of how the data can be manipulated). - * $('#example').dataTable( { - * "ajax": { - * "url": "data.json", - * "dataSrc": function ( json ) { - * for ( var i=0, ien=json.length ; i<ien ; i++ ) { - * json[i][0] = '<a href="/message/'+json[i][0]+'>View message</a>'; - * } - * return json; - * } - * } - * } ); - * - * @example - * // Add data to the request - * $('#example').dataTable( { - * "ajax": { - * "url": "data.json", - * "data": function ( d ) { - * return { - * "extra_search": $('#extra').val() - * }; - * } - * } - * } ); - * - * @example - * // Send request as POST - * $('#example').dataTable( { - * "ajax": { - * "url": "data.json", - * "type": "POST" - * } - * } ); - * - * @example - * // Get the data from localStorage (could interface with a form for - * // adding, editing and removing rows). - * $('#example').dataTable( { - * "ajax": function (data, callback, settings) { - * callback( - * JSON.parse( localStorage.getItem('dataTablesData') ) - * ); - * } - * } ); + * Flag to indicate if the search term should be interpreted as a + * regular expression (true) or not (false) and therefore and special + * regex characters escaped. */ - "ajax": null, + "regex": false, + /** + * Flag to indicate if DataTables is to use its smart filtering or not. + */ + "smart": true, /** - * This parameter allows you to readily specify the entries in the length drop - * down menu that DataTables shows when pagination is enabled. It can be - * either a 1D array of options which will be used for both the displayed - * option and the value, or a 2D array which will use the array in the first - * position as the value, and the array in the second position as the - * displayed options (useful for language strings such as 'All'). - * - * Note that the `pageLength` property will be automatically set to the - * first value given in this array, unless `pageLength` is also provided. - * @type array - * @default [ 10, 25, 50, 100 ] - * - * @dtopt Option - * @name DataTable.defaults.lengthMenu - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "All"]] - * } ); - * } ); + * Flag to indicate if DataTables should only trigger a search when + * the return key is pressed. */ - "aLengthMenu": [ 10, 25, 50, 100 ], + "return": false + }; + + + /** + * Template object for the way in which DataTables holds information about + * each individual row. This is the object format used for the settings + * aoData array. + * @namespace + */ + DataTable.models.oRow = { /** - * The `columns` option in the initialisation parameter allows you to define - * details about the way individual columns behave. For a full list of - * column options that can be set, please see - * {@link DataTable.defaults.column}. Note that if you use `columns` to - * define your columns, you must have an entry in the array for every single - * column that you have in your table (these can be null if you don't which - * to specify any options). - * @member - * - * @name DataTable.defaults.column + * TR element for the row */ - "aoColumns": null, + "nTr": null, /** - * Very similar to `columns`, `columnDefs` allows you to target a specific - * column, multiple columns, or all columns, using the `targets` property of - * each object in the array. This allows great flexibility when creating - * tables, as the `columnDefs` arrays can be of any length, targeting the - * columns you specifically want. `columnDefs` may use any of the column - * options available: {@link DataTable.defaults.column}, but it _must_ - * have `targets` defined in each object in the array. Values in the `targets` - * array may be: - * <ul> - * <li>a string - class name will be matched on the TH for the column</li> - * <li>0 or a positive integer - column index counting from the left</li> - * <li>a negative integer - column index counting from the right</li> - * <li>the string "_all" - all columns (i.e. assign a default)</li> - * </ul> - * @member - * - * @name DataTable.defaults.columnDefs + * Array of TD elements for each row. This is null until the row has been + * created. */ - "aoColumnDefs": null, - + "anCells": null, /** - * Basically the same as `search`, this parameter defines the individual column - * filtering state at initialisation time. The array must be of the same size - * as the number of columns, and each element be an object with the parameters - * `search` and `escapeRegex` (the latter is optional). 'null' is also - * accepted and the default will be used. - * @type array - * @default [] - * - * @dtopt Option - * @name DataTable.defaults.searchCols - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "searchCols": [ - * null, - * { "search": "My filter" }, - * null, - * { "search": "^[0-9]", "escapeRegex": false } - * ] - * } ); - * } ) + * Data object from the original data source for the row. This is either + * an array if using the traditional form of DataTables, or an object if + * using mData options. The exact type will depend on the passed in + * data from the data source, or will be an array if using DOM a data + * source. */ - "aoSearchCols": [], + "_aData": [], + /** + * Sorting data cache - this array is ostensibly the same length as the + * number of columns (although each index is generated only as it is + * needed), and holds the data that is used for sorting each column in the + * row. We do this cache generation at the start of the sort in order that + * the formatting of the sort data need be done only once for each cell + * per sort. This array should not be read from or written to by anything + * other than the master sorting methods. + */ + "_aSortData": null, /** - * An array of CSS classes that should be applied to displayed rows. This - * array may be of any length, and DataTables will apply each class - * sequentially, looping when required. - * @type array - * @default null <i>Will take the values determined by the `oClasses.stripe*` - * options</i> - * - * @dtopt Option - * @name DataTable.defaults.stripeClasses - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "stripeClasses": [ 'strip1', 'strip2', 'strip3' ] - * } ); - * } ) + * Per cell filtering data cache. As per the sort data cache, used to + * increase the performance of the filtering in DataTables */ - "asStripeClasses": null, + "_aFilterData": null, + /** + * Filtering data cache. This is the same as the cell filtering cache, but + * in this case a string rather than an array. This is easily computed with + * a join on `_aFilterData`, but is provided as a cache so the join isn't + * needed on every search (memory traded for performance) + */ + "_sFilterRow": null, /** - * Enable or disable automatic column width calculation. This can be disabled - * as an optimisation (it takes some time to calculate the widths) if the - * tables widths are passed in using `columns`. - * @type boolean - * @default true - * - * @dtopt Features - * @name DataTable.defaults.autoWidth - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "autoWidth": false - * } ); - * } ); + * Denote if the original data source was from the DOM, or the data source + * object. This is used for invalidating data, so DataTables can + * automatically read data from the original source, unless uninstructed + * otherwise. */ - "bAutoWidth": true, + "src": null, + /** + * Index in the aoData array. This saves an indexOf lookup when we have the + * object, but want to know the index + */ + "idx": -1, /** - * Deferred rendering can provide DataTables with a huge speed boost when you - * are using an Ajax or JS data source for the table. This option, when set to - * true, will cause DataTables to defer the creation of the table elements for - * each row until they are needed for a draw - saving a significant amount of - * time. - * @type boolean - * @default false - * - * @dtopt Features - * @name DataTable.defaults.deferRender - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "ajax": "sources/arrays.txt", - * "deferRender": true - * } ); - * } ); + * Cached display value */ - "bDeferRender": false, + displayData: null + }; + /** + * Template object for the column information object in DataTables. This object + * is held in the settings aoColumns array and contains all the information that + * DataTables needs about each individual column. + * + * Note that this object is related to {@link DataTable.defaults.column} + * but this one is the internal data store for DataTables's cache of columns. + * It should NOT be manipulated outside of DataTables. Any configuration should + * be done through the initialisation options. + * @namespace + */ + DataTable.models.oColumn = { /** - * Replace a DataTable which matches the given selector and replace it with - * one which has the properties of the new initialisation object passed. If no - * table matches the selector, then the new DataTable will be constructed as - * per normal. - * @type boolean - * @default false - * - * @dtopt Options - * @name DataTable.defaults.destroy - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "srollY": "200px", - * "paginate": false - * } ); - * - * // Some time later.... - * $('#example').dataTable( { - * "filter": false, - * "destroy": true - * } ); - * } ); + * Column index. */ - "bDestroy": false, - + "idx": null, /** - * Enable or disable filtering of data. Filtering in DataTables is "smart" in - * that it allows the end user to input multiple words (space separated) and - * will match a row containing those words, even if not in the order that was - * specified (this allow matching across multiple columns). Note that if you - * wish to use filtering in DataTables this must remain 'true' - to remove the - * default filtering input box and retain filtering abilities, please use - * {@link DataTable.defaults.dom}. - * @type boolean - * @default true - * - * @dtopt Features - * @name DataTable.defaults.searching - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "searching": false - * } ); - * } ); + * A list of the columns that sorting should occur on when this column + * is sorted. That this property is an array allows multi-column sorting + * to be defined for a column (for example first name / last name columns + * would benefit from this). The values are integers pointing to the + * columns to be sorted on (typically it will be a single integer pointing + * at itself, but that doesn't need to be the case). */ - "bFilter": true, - + "aDataSort": null, /** - * Enable or disable the table information display. This shows information - * about the data that is currently visible on the page, including information - * about filtered data if that action is being performed. - * @type boolean - * @default true - * - * @dtopt Features - * @name DataTable.defaults.info - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "info": false - * } ); - * } ); + * Define the sorting directions that are applied to the column, in sequence + * as the column is repeatedly sorted upon - i.e. the first value is used + * as the sorting direction when the column if first sorted (clicked on). + * Sort it again (click again) and it will move on to the next index. + * Repeat until loop. */ - "bInfo": true, - + "asSorting": null, /** - * Allows the end user to select the size of a formatted page from a select - * menu (sizes are 10, 25, 50 and 100). Requires pagination (`paginate`). - * @type boolean - * @default true - * - * @dtopt Features - * @name DataTable.defaults.lengthChange - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "lengthChange": false - * } ); - * } ); + * Flag to indicate if the column is searchable, and thus should be included + * in the filtering or not. */ - "bLengthChange": true, - + "bSearchable": null, /** - * Enable or disable pagination. - * @type boolean - * @default true - * - * @dtopt Features - * @name DataTable.defaults.paging - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "paging": false - * } ); - * } ); + * Flag to indicate if the column is sortable or not. */ - "bPaginate": true, - + "bSortable": null, /** - * Enable or disable the display of a 'processing' indicator when the table is - * being processed (e.g. a sort). This is particularly useful for tables with - * large amounts of data where it can take a noticeable amount of time to sort - * the entries. - * @type boolean - * @default false - * - * @dtopt Features - * @name DataTable.defaults.processing - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "processing": true - * } ); - * } ); + * Flag to indicate if the column is currently visible in the table or not */ - "bProcessing": false, - + "bVisible": null, /** - * Retrieve the DataTables object for the given selector. Note that if the - * table has already been initialised, this parameter will cause DataTables - * to simply return the object that has already been set up - it will not take - * account of any changes you might have made to the initialisation object - * passed to DataTables (setting this parameter to true is an acknowledgement - * that you understand this). `destroy` can be used to reinitialise a table if - * you need. - * @type boolean - * @default false - * - * @dtopt Options - * @name DataTable.defaults.retrieve - * - * @example - * $(document).ready( function() { - * initTable(); - * tableActions(); - * } ); - * - * function initTable () - * { - * return $('#example').dataTable( { - * "scrollY": "200px", - * "paginate": false, - * "retrieve": true - * } ); - * } - * - * function tableActions () - * { - * var table = initTable(); - * // perform API operations with oTable - * } + * Store for manual type assignment using the `column.type` option. This + * is held in store so we can manipulate the column's `sType` property. */ - "bRetrieve": false, - + "_sManualType": null, /** - * When vertical (y) scrolling is enabled, DataTables will force the height of - * the table's viewport to the given height at all times (useful for layout). - * However, this can look odd when filtering data down to a small data set, - * and the footer is left "floating" further down. This parameter (when - * enabled) will cause DataTables to collapse the table's viewport down when - * the result set will fit within the given Y height. - * @type boolean - * @default false - * - * @dtopt Options - * @name DataTable.defaults.scrollCollapse - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "scrollY": "200", - * "scrollCollapse": true - * } ); - * } ); + * Flag to indicate if HTML5 data attributes should be used as the data + * source for filtering or sorting. True is either are. */ - "bScrollCollapse": false, - + "_bAttrSrc": false, /** - * Configure DataTables to use server-side processing. Note that the - * `ajax` parameter must also be given in order to give DataTables a - * source to obtain the required data for each draw. - * @type boolean - * @default false - * - * @dtopt Features - * @dtopt Server-side - * @name DataTable.defaults.serverSide - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "serverSide": true, - * "ajax": "xhr.php" - * } ); - * } ); + * Developer definable function that is called whenever a cell is created (Ajax source, + * etc) or processed for input (DOM source). This can be used as a compliment to mRender + * allowing you to modify the DOM element (add background colour for example) when the + * element is available. */ - "bServerSide": false, - + "fnCreatedCell": null, /** - * Enable or disable sorting of columns. Sorting of individual columns can be - * disabled by the `sortable` option for each column. - * @type boolean - * @default true - * - * @dtopt Features - * @name DataTable.defaults.ordering - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "ordering": false - * } ); - * } ); + * Function to get data from a cell in a column. You should <b>never</b> + * access data directly through _aData internally in DataTables - always use + * the method attached to this property. It allows mData to function as + * required. This function is automatically assigned by the column + * initialisation method */ - "bSort": true, - + "fnGetData": null, /** - * Enable or display DataTables' ability to sort multiple columns at the - * same time (activated by shift-click by the user). - * @type boolean - * @default true - * - * @dtopt Options - * @name DataTable.defaults.orderMulti - * - * @example - * // Disable multiple column sorting ability - * $(document).ready( function () { - * $('#example').dataTable( { - * "orderMulti": false - * } ); - * } ); + * Function to set data for a cell in the column. You should <b>never</b> + * set the data directly to _aData internally in DataTables - always use + * this method. It allows mData to function as required. This function + * is automatically assigned by the column initialisation method */ - "bSortMulti": true, - + "fnSetData": null, /** - * Allows control over whether DataTables should use the top (true) unique - * cell that is found for a single column, or the bottom (false - default). - * This is useful when using complex headers. - * @type boolean - * @default false - * - * @dtopt Options - * @name DataTable.defaults.orderCellsTop - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "orderCellsTop": true - * } ); - * } ); + * Property to read the value for the cells in the column from the data + * source array / object. If null, then the default content is used, if a + * function is given then the return from the function is used. */ - "bSortCellsTop": false, - + "mData": null, /** - * Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and - * `sorting\_3` to the columns which are currently being sorted on. This is - * presented as a feature switch as it can increase processing time (while - * classes are removed and added) so for large data sets you might want to - * turn this off. - * @type boolean - * @default true - * - * @dtopt Features - * @name DataTable.defaults.orderClasses - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "orderClasses": false - * } ); - * } ); + * Partner property to mData which is used (only when defined) to get + * the data - i.e. it is basically the same as mData, but without the + * 'set' option, and also the data fed to it is the result from mData. + * This is the rendering method to match the data method of mData. */ - "bSortClasses": true, - + "mRender": null, /** - * Enable or disable state saving. When enabled HTML5 `localStorage` will be - * used to save table display information such as pagination information, - * display length, filtering and sorting. As such when the end user reloads - * the page the display display will match what thy had previously set up. - * - * Due to the use of `localStorage` the default state saving is not supported - * in IE6 or 7. If state saving is required in those browsers, use - * `stateSaveCallback` to provide a storage solution such as cookies. - * @type boolean - * @default false - * - * @dtopt Features - * @name DataTable.defaults.stateSave - * - * @example - * $(document).ready( function () { - * $('#example').dataTable( { - * "stateSave": true - * } ); - * } ); + * The class to apply to all TD elements in the table's TBODY for the column */ - "bStateSave": false, - + "sClass": null, /** - * This function is called when a TR element is created (and all TD child - * elements have been inserted), or registered if using a DOM source, allowing - * manipulation of the TR element (adding classes etc). - * @type function - * @param {node} row "TR" element for the current row - * @param {array} data Raw data array for this row - * @param {int} dataIndex The index of this row in the internal aoData array - * - * @dtopt Callbacks - * @name DataTable.defaults.createdRow - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "createdRow": function( row, data, dataIndex ) { - * // Bold the grade for all 'A' grade browsers - * if ( data[4] == "A" ) - * { - * $('td:eq(4)', row).html( '<b>A</b>' ); - * } - * } - * } ); - * } ); + * When DataTables calculates the column widths to assign to each column, + * it finds the longest string in each column and then constructs a + * temporary table and reads the widths from that. The problem with this + * is that "mmm" is much wider then "iiii", but the latter is a longer + * string - thus the calculation can go wrong (doing it properly and putting + * it into an DOM object and measuring that is horribly(!) slow). Thus as + * a "work around" we provide this option. It will append its value to the + * text that is found to be the longest string for the column - i.e. padding. */ - "fnCreatedRow": null, + "sContentPadding": null, + /** + * Allows a default value to be given for a column's data, and will be used + * whenever a null data source is encountered (this can be because mData + * is set to null, or because the data source itself is null). + */ + "sDefaultContent": null, /** - * This function is called on every 'draw' event, and allows you to - * dynamically modify any aspect you want about the created DOM. - * @type function - * @param {object} settings DataTables settings object - * - * @dtopt Callbacks - * @name DataTable.defaults.drawCallback - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "drawCallback": function( settings ) { - * alert( 'DataTables has redrawn the table' ); - * } - * } ); - * } ); + * Name for the column, allowing reference to the column by name as well as + * by index (needs a lookup to work by name). */ - "fnDrawCallback": null, + "sName": null, + /** + * Custom sorting data type - defines which of the available plug-ins in + * afnSortData the custom sorting will use - if any is defined. + */ + "sSortDataType": 'std', /** - * Identical to fnHeaderCallback() but for the table footer this function - * allows you to modify the table footer on every 'draw' event. - * @type function - * @param {node} foot "TR" element for the footer - * @param {array} data Full table data (as derived from the original HTML) - * @param {int} start Index for the current display starting point in the - * display array - * @param {int} end Index for the current display ending point in the - * display array - * @param {array int} display Index array to translate the visual position - * to the full data array - * - * @dtopt Callbacks - * @name DataTable.defaults.footerCallback - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "footerCallback": function( tfoot, data, start, end, display ) { - * tfoot.getElementsByTagName('th')[0].innerHTML = "Starting index is "+start; - * } - * } ); - * } ) + * Class to be applied to the header element when sorting on this column */ - "fnFooterCallback": null, + "sSortingClass": null, + /** + * Title of the column - what is seen in the TH element (nTh). + */ + "sTitle": null, /** - * When rendering large numbers in the information element for the table - * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers - * to have a comma separator for the 'thousands' units (e.g. 1 million is - * rendered as "1,000,000") to help readability for the end user. This - * function will override the default method DataTables uses. - * @type function - * @member - * @param {int} toFormat number to be formatted - * @returns {string} formatted string for DataTables to show the number - * - * @dtopt Callbacks - * @name DataTable.defaults.formatNumber - * - * @example - * // Format a number using a single quote for the separator (note that - * // this can also be done with the language.thousands option) - * $(document).ready( function() { - * $('#example').dataTable( { - * "formatNumber": function ( toFormat ) { - * return toFormat.toString().replace( - * /\B(?=(\d{3})+(?!\d))/g, "'" - * ); - * }; - * } ); - * } ); + * Column sorting and filtering type */ - "fnFormatNumber": function ( toFormat ) { - return toFormat.toString().replace( - /\B(?=(\d{3})+(?!\d))/g, - this.oLanguage.sThousands - ); - }, + "sType": null, + /** + * Width of the column + */ + "sWidth": null, /** - * This function is called on every 'draw' event, and allows you to - * dynamically modify the header row. This can be used to calculate and - * display useful information about the table. - * @type function - * @param {node} head "TR" element for the header - * @param {array} data Full table data (as derived from the original HTML) - * @param {int} start Index for the current display starting point in the - * display array - * @param {int} end Index for the current display ending point in the - * display array - * @param {array int} display Index array to translate the visual position - * to the full data array - * - * @dtopt Callbacks - * @name DataTable.defaults.headerCallback - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "fheaderCallback": function( head, data, start, end, display ) { - * head.getElementsByTagName('th')[0].innerHTML = "Displaying "+(end-start)+" records"; - * } - * } ); - * } ) + * Width of the column when it was first "encountered" */ - "fnHeaderCallback": null, + "sWidthOrig": null, + /** Cached string which is the longest in the column */ + maxLenString: null, /** - * The information element can be used to convey information about the current - * state of the table. Although the internationalisation options presented by - * DataTables are quite capable of dealing with most customisations, there may - * be times where you wish to customise the string further. This callback - * allows you to do exactly that. - * @type function - * @param {object} oSettings DataTables settings object - * @param {int} start Starting position in data for the draw - * @param {int} end End position in data for the draw - * @param {int} max Total number of rows in the table (regardless of - * filtering) - * @param {int} total Total number of rows in the data set, after filtering - * @param {string} pre The string that DataTables has formatted using it's - * own rules - * @returns {string} The string to be displayed in the information element. - * - * @dtopt Callbacks - * @name DataTable.defaults.infoCallback - * - * @example - * $('#example').dataTable( { - * "infoCallback": function( settings, start, end, max, total, pre ) { - * return start +" to "+ end; - * } - * } ); + * Store for named searches */ - "fnInfoCallback": null, + searchFixed: null + }; + /* + * Developer note: The properties of the object below are given in Hungarian + * notation, that was used as the interface for DataTables prior to v1.10, however + * from v1.10 onwards the primary interface is camel case. In order to avoid + * breaking backwards compatibility utterly with this change, the Hungarian + * version is still, internally the primary interface, but is is not documented + * - hence the @name tags in each doc comment. This allows a Javascript function + * to create a map from Hungarian notation to camel case (going the other direction + * would require each property to be listed, which would add around 3K to the size + * of DataTables, while this method is about a 0.5K hit). + * + * Ultimately this does pave the way for Hungarian notation to be dropped + * completely, but that is a massive amount of work and will break current + * installs (therefore is on-hold until v2). + */ + + /** + * Initialisation options that can be given to DataTables at initialisation + * time. + * @namespace + */ + DataTable.defaults = { /** - * Called when the table has been initialised. Normally DataTables will - * initialise sequentially and there will be no need for this function, - * however, this does not hold true when using external language information - * since that is obtained using an async XHR call. - * @type function - * @param {object} settings DataTables settings object - * @param {object} json The JSON object request from the server - only - * present if client-side Ajax sourced data is used - * - * @dtopt Callbacks - * @name DataTable.defaults.initComplete - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "initComplete": function(settings, json) { - * alert( 'DataTables has finished its initialisation.' ); - * } - * } ); - * } ) + * An array of data to use for the table, passed in at initialisation which + * will be used in preference to any data which is already in the DOM. This is + * particularly useful for constructing tables purely in Javascript, for + * example with a custom Ajax call. */ - "fnInitComplete": null, + "aaData": null, /** - * Called at the very start of each table draw and can be used to cancel the - * draw by returning false, any other return (including undefined) results in - * the full draw occurring). - * @type function - * @param {object} settings DataTables settings object - * @returns {boolean} False will cancel the draw, anything else (including no - * return) will allow it to complete. - * - * @dtopt Callbacks - * @name DataTable.defaults.preDrawCallback - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "preDrawCallback": function( settings ) { - * if ( $('#test').val() == 1 ) { - * return false; - * } - * } - * } ); - * } ); + * If ordering is enabled, then DataTables will perform a first pass sort on + * initialisation. You can define which column(s) the sort is performed + * upon, and the sorting direction, with this variable. The `sorting` array + * should contain an array for each column to be sorted initially containing + * the column's index and a direction string ('asc' or 'desc'). */ - "fnPreDrawCallback": null, + "aaSorting": [[0,'asc']], /** - * This function allows you to 'post process' each row after it have been - * generated for each table draw, but before it is rendered on screen. This - * function might be used for setting the row class name etc. - * @type function - * @param {node} row "TR" element for the current row - * @param {array} data Raw data array for this row - * @param {int} displayIndex The display index for the current table draw - * @param {int} displayIndexFull The index of the data in the full list of - * rows (after filtering) - * - * @dtopt Callbacks - * @name DataTable.defaults.rowCallback - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "rowCallback": function( row, data, displayIndex, displayIndexFull ) { - * // Bold the grade for all 'A' grade browsers - * if ( data[4] == "A" ) { - * $('td:eq(4)', row).html( '<b>A</b>' ); - * } - * } - * } ); - * } ); + * This parameter is basically identical to the `sorting` parameter, but + * cannot be overridden by user interaction with the table. What this means + * is that you could have a column (visible or hidden) which the sorting + * will always be forced on first - any sorting after that (from the user) + * will then be performed as required. This can be useful for grouping rows + * together. */ - "fnRowCallback": null, + "aaSortingFixed": [], /** - * __Deprecated__ The functionality provided by this parameter has now been - * superseded by that provided through `ajax`, which should be used instead. + * DataTables can be instructed to load data to display in the table from a + * Ajax source. This option defines how that Ajax call is made and where to. * - * This parameter allows you to override the default function which obtains - * the data from the server so something more suitable for your application. - * For example you could use POST data, or pull information from a Gears or - * AIR database. - * @type function - * @member - * @param {string} source HTTP source to obtain the data from (`ajax`) - * @param {array} data A key/value pair object containing the data to send - * to the server - * @param {function} callback to be called on completion of the data get - * process that will draw the data on the page. - * @param {object} settings DataTables settings object + * The `ajax` property has three different modes of operation, depending on + * how it is defined. These are: * - * @dtopt Callbacks - * @dtopt Server-side - * @name DataTable.defaults.serverData + * * `string` - Set the URL from where the data should be loaded from. + * * `object` - Define properties for `jQuery.ajax`. + * * `function` - Custom data get function * - * @deprecated 1.10. Please use `ajax` for this functionality now. - */ - "fnServerData": null, - - - /** - * __Deprecated__ The functionality provided by this parameter has now been - * superseded by that provided through `ajax`, which should be used instead. + * `string` + * -------- * - * It is often useful to send extra data to the server when making an Ajax - * request - for example custom filtering information, and this callback - * function makes it trivial to send extra information to the server. The - * passed in parameter is the data set that has been constructed by - * DataTables, and you can add to this or modify it as you require. - * @type function - * @param {array} data Data array (array of objects which are name/value - * pairs) that has been constructed by DataTables and will be sent to the - * server. In the case of Ajax sourced data with server-side processing - * this will be an empty array, for server-side processing there will be a - * significant number of parameters! - * @returns {undefined} Ensure that you modify the data array passed in, - * as this is passed by reference. + * As a string, the `ajax` property simply defines the URL from which + * DataTables will load data. * - * @dtopt Callbacks - * @dtopt Server-side - * @name DataTable.defaults.serverParams + * `object` + * -------- * - * @deprecated 1.10. Please use `ajax` for this functionality now. - */ - "fnServerParams": null, - - - /** - * Load the table state. With this function you can define from where, and how, the - * state of a table is loaded. By default DataTables will load from `localStorage` - * but you might wish to use a server-side database or cookies. - * @type function - * @member - * @param {object} settings DataTables settings object - * @param {object} callback Callback that can be executed when done. It - * should be passed the loaded state object. - * @return {object} The DataTables state object to be loaded + * As an object, the parameters in the object are passed to + * [jQuery.ajax](https://api.jquery.com/jQuery.ajax/) allowing fine control + * of the Ajax request. DataTables has a number of default parameters which + * you can override using this option. Please refer to the jQuery + * documentation for a full description of the options available, although + * the following parameters provide additional options in DataTables or + * require special consideration: + * + * * `data` - As with jQuery, `data` can be provided as an object, but it + * can also be used as a function to manipulate the data DataTables sends + * to the server. The function takes a single parameter, an object of + * parameters with the values that DataTables has readied for sending. An + * object may be returned which will be merged into the DataTables + * defaults, or you can add the items to the object that was passed in and + * not return anything from the function. This supersedes `fnServerParams` + * from DataTables 1.9-. * - * @dtopt Callbacks - * @name DataTable.defaults.stateLoadCallback + * * `dataSrc` - By default DataTables will look for the property `data` (or + * `aaData` for compatibility with DataTables 1.9-) when obtaining data + * from an Ajax source or for server-side processing - this parameter + * allows that property to be changed. You can use Javascript dotted + * object notation to get a data source for multiple levels of nesting, or + * it my be used as a function. As a function it takes a single parameter, + * the JSON returned from the server, which can be manipulated as + * required, with the returned value being that used by DataTables as the + * data source for the table. * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "stateSave": true, - * "stateLoadCallback": function (settings, callback) { - * $.ajax( { - * "url": "/state_load", - * "dataType": "json", - * "success": function (json) { - * callback( json ); - * } - * } ); - * } - * } ); - * } ); - */ - "fnStateLoadCallback": function ( settings ) { - try { - return JSON.parse( - (settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem( - 'DataTables_'+settings.sInstance+'_'+location.pathname - ) - ); - } catch (e) { - return {}; - } - }, - - - /** - * Callback which allows modification of the saved state prior to loading that state. - * This callback is called when the table is loading state from the stored data, but - * prior to the settings object being modified by the saved state. Note that for - * plug-in authors, you should use the `stateLoadParams` event to load parameters for - * a plug-in. - * @type function - * @param {object} settings DataTables settings object - * @param {object} data The state object that is to be loaded + * * `success` - Should not be overridden it is used internally in + * DataTables. To manipulate / transform the data returned by the server + * use `ajax.dataSrc`, or use `ajax` as a function (see below). * - * @dtopt Callbacks - * @name DataTable.defaults.stateLoadParams + * `function` + * ---------- * - * @example - * // Remove a saved filter, so filtering is never loaded - * $(document).ready( function() { - * $('#example').dataTable( { - * "stateSave": true, - * "stateLoadParams": function (settings, data) { - * data.oSearch.sSearch = ""; - * } - * } ); - * } ); + * As a function, making the Ajax call is left up to yourself allowing + * complete control of the Ajax request. Indeed, if desired, a method other + * than Ajax could be used to obtain the required data, such as Web storage + * or an AIR database. * - * @example - * // Disallow state loading by returning false - * $(document).ready( function() { - * $('#example').dataTable( { - * "stateSave": true, - * "stateLoadParams": function (settings, data) { - * return false; - * } - * } ); - * } ); + * The function is given four parameters and no return is required. The + * parameters are: + * + * 1. _object_ - Data to send to the server + * 2. _function_ - Callback function that must be executed when the required + * data has been obtained. That data should be passed into the callback + * as the only parameter + * 3. _object_ - DataTables settings object for the table */ - "fnStateLoadParams": null, + "ajax": null, /** - * Callback that is called when the state has been loaded from the state saving method - * and the DataTables settings object has been modified as a result of the loaded state. - * @type function - * @param {object} settings DataTables settings object - * @param {object} data The state object that was loaded - * - * @dtopt Callbacks - * @name DataTable.defaults.stateLoaded + * This parameter allows you to readily specify the entries in the length drop + * down menu that DataTables shows when pagination is enabled. It can be + * either a 1D array of options which will be used for both the displayed + * option and the value, or a 2D array which will use the array in the first + * position as the value, and the array in the second position as the + * displayed options (useful for language strings such as 'All'). * - * @example - * // Show an alert with the filtering value that was saved - * $(document).ready( function() { - * $('#example').dataTable( { - * "stateSave": true, - * "stateLoaded": function (settings, data) { - * alert( 'Saved filter was: '+data.oSearch.sSearch ); - * } - * } ); - * } ); + * Note that the `pageLength` property will be automatically set to the + * first value given in this array, unless `pageLength` is also provided. */ - "fnStateLoaded": null, + "aLengthMenu": [ 10, 25, 50, 100 ], /** - * Save the table state. This function allows you to define where and how the state - * information for the table is stored By default DataTables will use `localStorage` - * but you might wish to use a server-side database or cookies. - * @type function - * @member - * @param {object} settings DataTables settings object - * @param {object} data The state object to be saved - * - * @dtopt Callbacks - * @name DataTable.defaults.stateSaveCallback - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "stateSave": true, - * "stateSaveCallback": function (settings, data) { - * // Send an Ajax request to the server with the state object - * $.ajax( { - * "url": "/state_save", - * "data": data, - * "dataType": "json", - * "method": "POST" - * "success": function () {} - * } ); - * } - * } ); - * } ); + * The `columns` option in the initialisation parameter allows you to define + * details about the way individual columns behave. For a full list of + * column options that can be set, please see + * {@link DataTable.defaults.column}. Note that if you use `columns` to + * define your columns, you must have an entry in the array for every single + * column that you have in your table (these can be null if you don't which + * to specify any options). */ - "fnStateSaveCallback": function ( settings, data ) { - try { - (settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem( - 'DataTables_'+settings.sInstance+'_'+location.pathname, - JSON.stringify( data ) - ); - } catch (e) {} - }, + "aoColumns": null, + + /** + * Very similar to `columns`, `columnDefs` allows you to target a specific + * column, multiple columns, or all columns, using the `targets` property of + * each object in the array. This allows great flexibility when creating + * tables, as the `columnDefs` arrays can be of any length, targeting the + * columns you specifically want. `columnDefs` may use any of the column + * options available: {@link DataTable.defaults.column}, but it _must_ + * have `targets` defined in each object in the array. Values in the `targets` + * array may be: + * <ul> + * <li>a string - class name will be matched on the TH for the column</li> + * <li>0 or a positive integer - column index counting from the left</li> + * <li>a negative integer - column index counting from the right</li> + * <li>the string "_all" - all columns (i.e. assign a default)</li> + * </ul> + */ + "aoColumnDefs": null, /** - * Callback which allows modification of the state to be saved. Called when the table - * has changed state a new state save is required. This method allows modification of - * the state saving object prior to actually doing the save, including addition or - * other state properties or modification. Note that for plug-in authors, you should - * use the `stateSaveParams` event to save parameters for a plug-in. - * @type function - * @param {object} settings DataTables settings object - * @param {object} data The state object to be saved - * - * @dtopt Callbacks - * @name DataTable.defaults.stateSaveParams - * - * @example - * // Remove a saved filter, so filtering is never saved - * $(document).ready( function() { - * $('#example').dataTable( { - * "stateSave": true, - * "stateSaveParams": function (settings, data) { - * data.oSearch.sSearch = ""; - * } - * } ); - * } ); + * Basically the same as `search`, this parameter defines the individual column + * filtering state at initialisation time. The array must be of the same size + * as the number of columns, and each element be an object with the parameters + * `search` and `escapeRegex` (the latter is optional). 'null' is also + * accepted and the default will be used. */ - "fnStateSaveParams": null, + "aoSearchCols": [], /** - * Duration for which the saved state information is considered valid. After this period - * has elapsed the state will be returned to the default. - * Value is given in seconds. - * @type int - * @default 7200 <i>(2 hours)</i> - * - * @dtopt Options - * @name DataTable.defaults.stateDuration - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "stateDuration": 60*60*24; // 1 day - * } ); - * } ) + * Enable or disable automatic column width calculation. This can be disabled + * as an optimisation (it takes some time to calculate the widths) if the + * tables widths are passed in using `columns`. */ - "iStateDuration": 7200, + "bAutoWidth": true, /** - * When enabled DataTables will not make a request to the server for the first - * page draw - rather it will use the data already on the page (no sorting etc - * will be applied to it), thus saving on an XHR at load time. `deferLoading` - * is used to indicate that deferred loading is required, but it is also used - * to tell DataTables how many records there are in the full table (allowing - * the information element and pagination to be displayed correctly). In the case - * where a filtering is applied to the table on initial load, this can be - * indicated by giving the parameter as an array, where the first element is - * the number of records available after filtering and the second element is the - * number of records without filtering (allowing the table information element - * to be shown correctly). - * @type int | array - * @default null - * - * @dtopt Options - * @name DataTable.defaults.deferLoading - * - * @example - * // 57 records available in the table, no filtering applied - * $(document).ready( function() { - * $('#example').dataTable( { - * "serverSide": true, - * "ajax": "scripts/server_processing.php", - * "deferLoading": 57 - * } ); - * } ); - * - * @example - * // 57 records after filtering, 100 without filtering (an initial filter applied) - * $(document).ready( function() { - * $('#example').dataTable( { - * "serverSide": true, - * "ajax": "scripts/server_processing.php", - * "deferLoading": [ 57, 100 ], - * "search": { - * "search": "my_filter" - * } - * } ); - * } ); + * Deferred rendering can provide DataTables with a huge speed boost when you + * are using an Ajax or JS data source for the table. This option, when set to + * true, will cause DataTables to defer the creation of the table elements for + * each row until they are needed for a draw - saving a significant amount of + * time. */ - "iDeferLoading": null, + "bDeferRender": true, /** - * Number of rows to display on a single page when using pagination. If - * feature enabled (`lengthChange`) then the end user will be able to override - * this to a custom setting using a pop-up menu. - * @type int - * @default 10 - * - * @dtopt Options - * @name DataTable.defaults.pageLength - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "pageLength": 50 - * } ); - * } ) + * Replace a DataTable which matches the given selector and replace it with + * one which has the properties of the new initialisation object passed. If no + * table matches the selector, then the new DataTable will be constructed as + * per normal. */ - "iDisplayLength": 10, + "bDestroy": false, /** - * Define the starting point for data display when using DataTables with - * pagination. Note that this parameter is the number of records, rather than - * the page number, so if you have 10 records per page and want to start on - * the third page, it should be "20". - * @type int - * @default 0 - * - * @dtopt Options - * @name DataTable.defaults.displayStart - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "displayStart": 20 - * } ); - * } ) + * Enable or disable filtering of data. Filtering in DataTables is "smart" in + * that it allows the end user to input multiple words (space separated) and + * will match a row containing those words, even if not in the order that was + * specified (this allow matching across multiple columns). Note that if you + * wish to use filtering in DataTables this must remain 'true' - to remove the + * default filtering input box and retain filtering abilities, please use + * {@link DataTable.defaults.dom}. */ - "iDisplayStart": 0, - + "bFilter": true, /** - * By default DataTables allows keyboard navigation of the table (sorting, paging, - * and filtering) by adding a `tabindex` attribute to the required elements. This - * allows you to tab through the controls and press the enter key to activate them. - * The tabindex is default 0, meaning that the tab follows the flow of the document. - * You can overrule this using this parameter if you wish. Use a value of -1 to - * disable built-in keyboard navigation. - * @type int - * @default 0 - * - * @dtopt Options - * @name DataTable.defaults.tabIndex - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "tabIndex": 1 - * } ); - * } ); + * Used only for compatiblity with DT1 + * @deprecated */ - "iTabIndex": 0, + "bInfo": true, + /** + * Used only for compatiblity with DT1 + * @deprecated + */ + "bLengthChange": true, /** - * Classes that DataTables assigns to the various components and features - * that it adds to the HTML table. This allows classes to be configured - * during initialisation in addition to through the static - * {@link DataTable.ext.oStdClasses} object). - * @namespace - * @name DataTable.defaults.classes + * Enable or disable pagination. */ - "oClasses": {}, + "bPaginate": true, /** - * All strings that DataTables uses in the user interface that it creates - * are defined in this object, allowing you to modified them individually or - * completely replace them all as required. - * @namespace - * @name DataTable.defaults.language + * Enable or disable the display of a 'processing' indicator when the table is + * being processed (e.g. a sort). This is particularly useful for tables with + * large amounts of data where it can take a noticeable amount of time to sort + * the entries. */ - "oLanguage": { - /** - * Strings that are used for WAI-ARIA labels and controls only (these are not - * actually visible on the page, but will be read by screenreaders, and thus - * must be internationalised as well). - * @namespace - * @name DataTable.defaults.language.aria - */ - "oAria": { - /** - * ARIA label that is added to the table headers when the column may be - * sorted ascending by activing the column (click or return when focused). - * Note that the column header is prefixed to this string. - * @type string - * @default : activate to sort column ascending - * - * @dtopt Language - * @name DataTable.defaults.language.aria.sortAscending - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "aria": { - * "sortAscending": " - click/return to sort ascending" - * } - * } - * } ); - * } ); - */ - "sSortAscending": ": activate to sort column ascending", + "bProcessing": false, - /** - * ARIA label that is added to the table headers when the column may be - * sorted descending by activing the column (click or return when focused). - * Note that the column header is prefixed to this string. - * @type string - * @default : activate to sort column ascending - * - * @dtopt Language - * @name DataTable.defaults.language.aria.sortDescending - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "aria": { - * "sortDescending": " - click/return to sort descending" - * } - * } - * } ); - * } ); - */ - "sSortDescending": ": activate to sort column descending" - }, - /** - * Pagination string used by DataTables for the built-in pagination - * control types. - * @namespace - * @name DataTable.defaults.language.paginate - */ - "oPaginate": { - /** - * Text to use when using the 'full_numbers' type of pagination for the - * button to take the user to the first page. - * @type string - * @default First - * - * @dtopt Language - * @name DataTable.defaults.language.paginate.first - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "paginate": { - * "first": "First page" - * } - * } - * } ); - * } ); - */ - "sFirst": "First", + /** + * Retrieve the DataTables object for the given selector. Note that if the + * table has already been initialised, this parameter will cause DataTables + * to simply return the object that has already been set up - it will not take + * account of any changes you might have made to the initialisation object + * passed to DataTables (setting this parameter to true is an acknowledgement + * that you understand this). `destroy` can be used to reinitialise a table if + * you need. + */ + "bRetrieve": false, - /** - * Text to use when using the 'full_numbers' type of pagination for the - * button to take the user to the last page. - * @type string - * @default Last - * - * @dtopt Language - * @name DataTable.defaults.language.paginate.last - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "paginate": { - * "last": "Last page" - * } - * } - * } ); - * } ); - */ - "sLast": "Last", + /** + * When vertical (y) scrolling is enabled, DataTables will force the height of + * the table's viewport to the given height at all times (useful for layout). + * However, this can look odd when filtering data down to a small data set, + * and the footer is left "floating" further down. This parameter (when + * enabled) will cause DataTables to collapse the table's viewport down when + * the result set will fit within the given Y height. + */ + "bScrollCollapse": false, - /** - * Text to use for the 'next' pagination button (to take the user to the - * next page). - * @type string - * @default Next - * - * @dtopt Language - * @name DataTable.defaults.language.paginate.next - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "paginate": { - * "next": "Next page" - * } - * } - * } ); - * } ); - */ - "sNext": "Next", + /** + * Configure DataTables to use server-side processing. Note that the + * `ajax` parameter must also be given in order to give DataTables a + * source to obtain the required data for each draw. + */ + "bServerSide": false, - /** - * Text to use for the 'previous' pagination button (to take the user to - * the previous page). - * @type string - * @default Previous - * - * @dtopt Language - * @name DataTable.defaults.language.paginate.previous - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "paginate": { - * "previous": "Previous page" - * } - * } - * } ); - * } ); - */ - "sPrevious": "Previous" - }, + /** + * Enable or disable sorting of columns. Sorting of individual columns can be + * disabled by the `sortable` option for each column. + */ + "bSort": true, - /** - * This string is shown in preference to `zeroRecords` when the table is - * empty of data (regardless of filtering). Note that this is an optional - * parameter - if it is not given, the value of `zeroRecords` will be used - * instead (either the default or given value). - * @type string - * @default No data available in table - * - * @dtopt Language - * @name DataTable.defaults.language.emptyTable - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "emptyTable": "No data available in table" - * } - * } ); - * } ); - */ - "sEmptyTable": "No data available in table", + /** + * Enable or display DataTables' ability to sort multiple columns at the + * same time (activated by shift-click by the user). + */ + "bSortMulti": true, - /** - * This string gives information to the end user about the information - * that is current on display on the page. The following tokens can be - * used in the string and will be dynamically replaced as the table - * display updates. This tokens can be placed anywhere in the string, or - * removed as needed by the language requires: - * - * * `\_START\_` - Display index of the first record on the current page - * * `\_END\_` - Display index of the last record on the current page - * * `\_TOTAL\_` - Number of records in the table after filtering - * * `\_MAX\_` - Number of records in the table without filtering - * * `\_PAGE\_` - Current page number - * * `\_PAGES\_` - Total number of pages of data in the table - * - * @type string - * @default Showing _START_ to _END_ of _TOTAL_ entries - * - * @dtopt Language - * @name DataTable.defaults.language.info - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "info": "Showing page _PAGE_ of _PAGES_" - * } - * } ); - * } ); - */ - "sInfo": "Showing _START_ to _END_ of _TOTAL_ entries", + /** + * Allows control over whether DataTables should use the top (true) unique + * cell that is found for a single column, or the bottom (false - default). + * This is useful when using complex headers. + */ + "bSortCellsTop": null, - /** - * Display information string for when the table is empty. Typically the - * format of this string should match `info`. - * @type string - * @default Showing 0 to 0 of 0 entries - * - * @dtopt Language - * @name DataTable.defaults.language.infoEmpty - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "infoEmpty": "No entries to show" - * } - * } ); - * } ); - */ - "sInfoEmpty": "Showing 0 to 0 of 0 entries", + /** + * Enable or disable the addition of the classes `sorting\_1`, `sorting\_2` and + * `sorting\_3` to the columns which are currently being sorted on. This is + * presented as a feature switch as it can increase processing time (while + * classes are removed and added) so for large data sets you might want to + * turn this off. + */ + "bSortClasses": true, - /** - * When a user filters the information in a table, this string is appended - * to the information (`info`) to give an idea of how strong the filtering - * is. The variable _MAX_ is dynamically updated. - * @type string - * @default (filtered from _MAX_ total entries) - * - * @dtopt Language - * @name DataTable.defaults.language.infoFiltered - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "infoFiltered": " - filtering from _MAX_ records" - * } - * } ); - * } ); - */ - "sInfoFiltered": "(filtered from _MAX_ total entries)", + /** + * Enable or disable state saving. When enabled HTML5 `localStorage` will be + * used to save table display information such as pagination information, + * display length, filtering and sorting. As such when the end user reloads + * the page the display display will match what thy had previously set up. + */ + "bStateSave": false, - /** - * If can be useful to append extra information to the info string at times, - * and this variable does exactly that. This information will be appended to - * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are - * being used) at all times. - * @type string - * @default <i>Empty string</i> - * - * @dtopt Language - * @name DataTable.defaults.language.infoPostFix - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "infoPostFix": "All records shown are derived from real information." - * } - * } ); - * } ); - */ - "sInfoPostFix": "", + + /** + * This function is called when a TR element is created (and all TD child + * elements have been inserted), or registered if using a DOM source, allowing + * manipulation of the TR element (adding classes etc). + */ + "fnCreatedRow": null, - /** - * This decimal place operator is a little different from the other - * language options since DataTables doesn't output floating point - * numbers, so it won't ever use this for display of a number. Rather, - * what this parameter does is modify the sort methods of the table so - * that numbers which are in a format which has a character other than - * a period (`.`) as a decimal place will be sorted numerically. - * - * Note that numbers with different decimal places cannot be shown in - * the same table and still be sortable, the table must be consistent. - * However, multiple different tables on the page can use different - * decimal place characters. - * @type string - * @default - * - * @dtopt Language - * @name DataTable.defaults.language.decimal - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "decimal": "," - * "thousands": "." - * } - * } ); - * } ); - */ - "sDecimal": "", + /** + * This function is called on every 'draw' event, and allows you to + * dynamically modify any aspect you want about the created DOM. + */ + "fnDrawCallback": null, - /** - * DataTables has a build in number formatter (`formatNumber`) which is - * used to format large numbers that are used in the table information. - * By default a comma is used, but this can be trivially changed to any - * character you wish with this parameter. - * @type string - * @default , - * - * @dtopt Language - * @name DataTable.defaults.language.thousands - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "thousands": "'" - * } - * } ); - * } ); - */ - "sThousands": ",", + /** + * Identical to fnHeaderCallback() but for the table footer this function + * allows you to modify the table footer on every 'draw' event. + */ + "fnFooterCallback": null, - /** - * Detail the action that will be taken when the drop down menu for the - * pagination length option is changed. The '_MENU_' variable is replaced - * with a default select list of 10, 25, 50 and 100, and can be replaced - * with a custom select box if required. - * @type string - * @default Show _MENU_ entries - * - * @dtopt Language - * @name DataTable.defaults.language.lengthMenu - * - * @example - * // Language change only - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "lengthMenu": "Display _MENU_ records" - * } - * } ); - * } ); - * - * @example - * // Language and options change - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "lengthMenu": 'Display <select>'+ - * '<option value="10">10</option>'+ - * '<option value="20">20</option>'+ - * '<option value="30">30</option>'+ - * '<option value="40">40</option>'+ - * '<option value="50">50</option>'+ - * '<option value="-1">All</option>'+ - * '</select> records' - * } - * } ); - * } ); - */ - "sLengthMenu": "Show _MENU_ entries", + /** + * When rendering large numbers in the information element for the table + * (i.e. "Showing 1 to 10 of 57 entries") DataTables will render large numbers + * to have a comma separator for the 'thousands' units (e.g. 1 million is + * rendered as "1,000,000") to help readability for the end user. This + * function will override the default method DataTables uses. + */ + "fnFormatNumber": function ( toFormat ) { + return toFormat.toString().replace( + /\B(?=(\d{3})+(?!\d))/g, + this.oLanguage.sThousands + ); + }, - /** - * When using Ajax sourced data and during the first draw when DataTables is - * gathering the data, this message is shown in an empty row in the table to - * indicate to the end user the the data is being loaded. Note that this - * parameter is not used when loading data by server-side processing, just - * Ajax sourced data with client-side processing. - * @type string - * @default Loading... - * - * @dtopt Language - * @name DataTable.defaults.language.loadingRecords - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "loadingRecords": "Please wait - loading..." - * } - * } ); - * } ); - */ - "sLoadingRecords": "Loading...", + /** + * This function is called on every 'draw' event, and allows you to + * dynamically modify the header row. This can be used to calculate and + * display useful information about the table. + */ + "fnHeaderCallback": null, - /** - * Text which is displayed when the table is processing a user action - * (usually a sort command or similar). - * @type string - * - * @dtopt Language - * @name DataTable.defaults.language.processing - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "processing": "DataTables is currently busy" - * } - * } ); - * } ); - */ - "sProcessing": "", + /** + * The information element can be used to convey information about the current + * state of the table. Although the internationalisation options presented by + * DataTables are quite capable of dealing with most customisations, there may + * be times where you wish to customise the string further. This callback + * allows you to do exactly that. + */ + "fnInfoCallback": null, - /** - * Details the actions that will be taken when the user types into the - * filtering input text box. The variable "_INPUT_", if used in the string, - * is replaced with the HTML text box for the filtering input allowing - * control over where it appears in the string. If "_INPUT_" is not given - * then the input box is appended to the string automatically. - * @type string - * @default Search: - * - * @dtopt Language - * @name DataTable.defaults.language.search - * - * @example - * // Input text box will be appended at the end automatically - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "search": "Filter records:" - * } - * } ); - * } ); - * - * @example - * // Specify where the filter should appear - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "search": "Apply filter _INPUT_ to table" - * } - * } ); - * } ); - */ - "sSearch": "Search:", + /** + * Called when the table has been initialised. Normally DataTables will + * initialise sequentially and there will be no need for this function, + * however, this does not hold true when using external language information + * since that is obtained using an async XHR call. + */ + "fnInitComplete": null, - /** - * Assign a `placeholder` attribute to the search `input` element - * @type string - * @default - * - * @dtopt Language - * @name DataTable.defaults.language.searchPlaceholder - */ - "sSearchPlaceholder": "", + /** + * Called at the very start of each table draw and can be used to cancel the + * draw by returning false, any other return (including undefined) results in + * the full draw occurring). + */ + "fnPreDrawCallback": null, - /** - * All of the language information can be stored in a file on the - * server-side, which DataTables will look up if this parameter is passed. - * It must store the URL of the language file, which is in a JSON format, - * and the object has the same properties as the oLanguage object in the - * initialiser object (i.e. the above parameters). Please refer to one of - * the example language files to see how this works in action. - * @type string - * @default <i>Empty string - i.e. disabled</i> - * - * @dtopt Language - * @name DataTable.defaults.language.url - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "url": "http://www.sprymedia.co.uk/dataTables/lang.txt" - * } - * } ); - * } ); - */ - "sUrl": "", + /** + * This function allows you to 'post process' each row after it have been + * generated for each table draw, but before it is rendered on screen. This + * function might be used for setting the row class name etc. + */ + "fnRowCallback": null, - /** - * Text shown inside the table records when the is no information to be - * displayed after filtering. `emptyTable` is shown when there is simply no - * information in the table at all (regardless of filtering). - * @type string - * @default No matching records found - * - * @dtopt Language - * @name DataTable.defaults.language.zeroRecords - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "language": { - * "zeroRecords": "No records to display" - * } - * } ); - * } ); - */ - "sZeroRecords": "No matching records found" + /** + * Load the table state. With this function you can define from where, and how, the + * state of a table is loaded. By default DataTables will load from `localStorage` + * but you might wish to use a server-side database or cookies. + */ + "fnStateLoadCallback": function ( settings ) { + try { + return JSON.parse( + (settings.iStateDuration === -1 ? sessionStorage : localStorage).getItem( + 'DataTables_'+settings.sInstance+'_'+location.pathname + ) + ); + } catch (e) { + return {}; + } }, /** - * This parameter allows you to have define the global filtering state at - * initialisation time. As an object the `search` parameter must be - * defined, but all other parameters are optional. When `regex` is true, - * the search string will be treated as a regular expression, when false - * (default) it will be treated as a straight string. When `smart` - * DataTables will use it's smart filtering methods (to word match at - * any point in the data), when false this will not be done. - * @namespace - * @extends DataTable.models.oSearch - * - * @dtopt Options - * @name DataTable.defaults.search - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "search": {"search": "Initial search"} - * } ); - * } ) + * Callback which allows modification of the saved state prior to loading that state. + * This callback is called when the table is loading state from the stored data, but + * prior to the settings object being modified by the saved state. Note that for + * plug-in authors, you should use the `stateLoadParams` event to load parameters for + * a plug-in. */ - "oSearch": $.extend( {}, DataTable.models.oSearch ), + "fnStateLoadParams": null, /** - * __Deprecated__ The functionality provided by this parameter has now been - * superseded by that provided through `ajax`, which should be used instead. - * - * By default DataTables will look for the property `data` (or `aaData` for - * compatibility with DataTables 1.9-) when obtaining data from an Ajax - * source or for server-side processing - this parameter allows that - * property to be changed. You can use Javascript dotted object notation to - * get a data source for multiple levels of nesting. - * @type string - * @default data - * - * @dtopt Options - * @dtopt Server-side - * @name DataTable.defaults.ajaxDataProp - * - * @deprecated 1.10. Please use `ajax` for this functionality now. + * Callback that is called when the state has been loaded from the state saving method + * and the DataTables settings object has been modified as a result of the loaded state. */ - "sAjaxDataProp": "data", + "fnStateLoaded": null, /** - * __Deprecated__ The functionality provided by this parameter has now been - * superseded by that provided through `ajax`, which should be used instead. - * - * You can instruct DataTables to load data from an external - * source using this parameter (use aData if you want to pass data in you - * already have). Simply provide a url a JSON object can be obtained from. - * @type string - * @default null - * - * @dtopt Options - * @dtopt Server-side - * @name DataTable.defaults.ajaxSource - * - * @deprecated 1.10. Please use `ajax` for this functionality now. + * Save the table state. This function allows you to define where and how the state + * information for the table is stored By default DataTables will use `localStorage` + * but you might wish to use a server-side database or cookies. */ - "sAjaxSource": null, + "fnStateSaveCallback": function ( settings, data ) { + try { + (settings.iStateDuration === -1 ? sessionStorage : localStorage).setItem( + 'DataTables_'+settings.sInstance+'_'+location.pathname, + JSON.stringify( data ) + ); + } catch (e) { + // noop + } + }, /** - * This initialisation variable allows you to specify exactly where in the - * DOM you want DataTables to inject the various controls it adds to the page - * (for example you might want the pagination controls at the top of the - * table). DIV elements (with or without a custom class) can also be added to - * aid styling. The follow syntax is used: - * <ul> - * <li>The following options are allowed: - * <ul> - * <li>'l' - Length changing</li> - * <li>'f' - Filtering input</li> - * <li>'t' - The table!</li> - * <li>'i' - Information</li> - * <li>'p' - Pagination</li> - * <li>'r' - pRocessing</li> - * </ul> - * </li> - * <li>The following constants are allowed: - * <ul> - * <li>'H' - jQueryUI theme "header" classes ('fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix')</li> - * <li>'F' - jQueryUI theme "footer" classes ('fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix')</li> - * </ul> - * </li> - * <li>The following syntax is expected: - * <ul> - * <li>'&lt;' and '&gt;' - div elements</li> - * <li>'&lt;"class" and '&gt;' - div with a class</li> - * <li>'&lt;"#id" and '&gt;' - div with an ID</li> - * </ul> - * </li> - * <li>Examples: - * <ul> - * <li>'&lt;"wrapper"flipt&gt;'</li> - * <li>'&lt;lf&lt;t&gt;ip&gt;'</li> - * </ul> - * </li> - * </ul> - * @type string - * @default lfrtip <i>(when `jQueryUI` is false)</i> <b>or</b> - * <"H"lfr>t<"F"ip> <i>(when `jQueryUI` is true)</i> - * - * @dtopt Options - * @name DataTable.defaults.dom - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "dom": '&lt;"top"i&gt;rt&lt;"bottom"flp&gt;&lt;"clear"&gt;' - * } ); - * } ); + * Callback which allows modification of the state to be saved. Called when the table + * has changed state a new state save is required. This method allows modification of + * the state saving object prior to actually doing the save, including addition or + * other state properties or modification. Note that for plug-in authors, you should + * use the `stateSaveParams` event to save parameters for a plug-in. */ - "sDom": "lfrtip", + "fnStateSaveParams": null, /** - * Search delay option. This will throttle full table searches that use the - * DataTables provided search input element (it does not effect calls to - * `dt-api search()`, providing a delay before the search is made. - * @type integer - * @default 0 - * - * @dtopt Options - * @name DataTable.defaults.searchDelay - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "searchDelay": 200 - * } ); - * } ) + * Duration for which the saved state information is considered valid. After this period + * has elapsed the state will be returned to the default. + * Value is given in seconds. */ - "searchDelay": null, + "iStateDuration": 7200, /** - * DataTables features six different built-in options for the buttons to - * display for pagination control: - * - * * `numbers` - Page number buttons only - * * `simple` - 'Previous' and 'Next' buttons only - * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers - * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons - * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers - * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers - * - * Further methods can be added using {@link DataTable.ext.oPagination}. - * @type string - * @default simple_numbers - * - * @dtopt Options - * @name DataTable.defaults.pagingType - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "pagingType": "full_numbers" - * } ); - * } ) + * Number of rows to display on a single page when using pagination. If + * feature enabled (`lengthChange`) then the end user will be able to override + * this to a custom setting using a pop-up menu. */ - "sPaginationType": "simple_numbers", + "iDisplayLength": 10, /** - * Enable horizontal scrolling. When a table is too wide to fit into a - * certain layout, or you have a large number of columns in the table, you - * can enable x-scrolling to show the table in a viewport, which can be - * scrolled. This property can be `true` which will allow the table to - * scroll horizontally when needed, or any CSS unit, or a number (in which - * case it will be treated as a pixel measurement). Setting as simply `true` - * is recommended. - * @type boolean|string - * @default <i>blank string - i.e. disabled</i> - * - * @dtopt Features - * @name DataTable.defaults.scrollX - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "scrollX": true, - * "scrollCollapse": true - * } ); - * } ); + * Define the starting point for data display when using DataTables with + * pagination. Note that this parameter is the number of records, rather than + * the page number, so if you have 10 records per page and want to start on + * the third page, it should be "20". */ - "sScrollX": "", + "iDisplayStart": 0, /** - * This property can be used to force a DataTable to use more width than it - * might otherwise do when x-scrolling is enabled. For example if you have a - * table which requires to be well spaced, this parameter is useful for - * "over-sizing" the table, and thus forcing scrolling. This property can by - * any CSS unit, or a number (in which case it will be treated as a pixel - * measurement). - * @type string - * @default <i>blank string - i.e. disabled</i> - * - * @dtopt Options - * @name DataTable.defaults.scrollXInner - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "scrollX": "100%", - * "scrollXInner": "110%" - * } ); - * } ); + * By default DataTables allows keyboard navigation of the table (sorting, paging, + * and filtering) by adding a `tabindex` attribute to the required elements. This + * allows you to tab through the controls and press the enter key to activate them. + * The tabindex is default 0, meaning that the tab follows the flow of the document. + * You can overrule this using this parameter if you wish. Use a value of -1 to + * disable built-in keyboard navigation. */ - "sScrollXInner": "", + "iTabIndex": 0, /** - * Enable vertical scrolling. Vertical scrolling will constrain the DataTable - * to the given height, and enable scrolling for any data which overflows the - * current viewport. This can be used as an alternative to paging to display - * a lot of data in a small area (although paging and scrolling can both be - * enabled at the same time). This property can be any CSS unit, or a number - * (in which case it will be treated as a pixel measurement). - * @type string - * @default <i>blank string - i.e. disabled</i> - * - * @dtopt Features - * @name DataTable.defaults.scrollY - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "scrollY": "200px", - * "paginate": false - * } ); - * } ); + * Classes that DataTables assigns to the various components and features + * that it adds to the HTML table. This allows classes to be configured + * during initialisation in addition to through the static + * {@link DataTable.ext.oStdClasses} object). */ - "sScrollY": "", + "oClasses": {}, /** - * __Deprecated__ The functionality provided by this parameter has now been - * superseded by that provided through `ajax`, which should be used instead. - * - * Set the HTTP method that is used to make the Ajax call for server-side - * processing or Ajax sourced data. - * @type string - * @default GET - * - * @dtopt Options - * @dtopt Server-side - * @name DataTable.defaults.serverMethod - * - * @deprecated 1.10. Please use `ajax` for this functionality now. + * All strings that DataTables uses in the user interface that it creates + * are defined in this object, allowing you to modified them individually or + * completely replace them all as required. */ - "sServerMethod": "GET", + "oLanguage": { + /** + * Strings that are used for WAI-ARIA labels and controls only (these are not + * actually visible on the page, but will be read by screenreaders, and thus + * must be internationalised as well). + */ + "oAria": { + /** + * ARIA label that is added to the table headers when the column may be sorted + */ + "orderable": ": Activate to sort", + /** + * ARIA label that is added to the table headers when the column is currently being sorted + */ + "orderableReverse": ": Activate to invert sorting", - /** - * DataTables makes use of renderers when displaying HTML elements for - * a table. These renderers can be added or modified by plug-ins to - * generate suitable mark-up for a site. For example the Bootstrap - * integration plug-in for DataTables uses a paging button renderer to - * display pagination buttons in the mark-up required by Bootstrap. - * - * For further information about the renderers available see - * DataTable.ext.renderer - * @type string|object - * @default null - * - * @name DataTable.defaults.renderer - * - */ - "renderer": null, + /** + * ARIA label that is added to the table headers when the column is currently being + * sorted and next step is to remove sorting + */ + "orderableRemove": ": Activate to remove sorting", + paginate: { + first: 'First', + last: 'Last', + next: 'Next', + previous: 'Previous' + } + }, - /** - * Set the data property name that DataTables should use to get a row's id - * to set as the `id` property in the node. - * @type string - * @default DT_RowId - * - * @name DataTable.defaults.rowId - */ - "rowId": "DT_RowId" - }; + /** + * Pagination string used by DataTables for the built-in pagination + * control types. + */ + "oPaginate": { + /** + * Label and character for first page button («) + */ + "sFirst": "\u00AB", - _fnHungarianMap( DataTable.defaults ); + /** + * Last page button (») + */ + "sLast": "\u00BB", + /** + * Next page button (›) + */ + "sNext": "\u203A", + /** + * Previous page button (‹) + */ + "sPrevious": "\u2039", + }, - /* - * Developer note - See note in model.defaults.js about the use of Hungarian - * notation and camel case. - */ + /** + * Plural object for the data type the table is showing + */ + entries: { + _: "entries", + 1: "entry" + }, - /** - * Column options that can be given to DataTables at initialisation time. - * @namespace - */ - DataTable.defaults.column = { - /** - * Define which column(s) an order will occur on for this column. This - * allows a column's ordering to take multiple columns into account when - * doing a sort or use the data from a different column. For example first - * name / last name columns make sense to do a multi-column sort over the - * two columns. - * @type array|int - * @default null <i>Takes the value of the column index automatically</i> - * - * @name DataTable.defaults.column.orderData - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "orderData": [ 0, 1 ], "targets": [ 0 ] }, - * { "orderData": [ 1, 0 ], "targets": [ 1 ] }, - * { "orderData": 2, "targets": [ 2 ] } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "orderData": [ 0, 1 ] }, - * { "orderData": [ 1, 0 ] }, - * { "orderData": 2 }, - * null, - * null - * ] - * } ); - * } ); - */ - "aDataSort": null, - "iDataSort": -1, + /** + * This string is shown in preference to `zeroRecords` when the table is + * empty of data (regardless of filtering). Note that this is an optional + * parameter - if it is not given, the value of `zeroRecords` will be used + * instead (either the default or given value). + */ + "sEmptyTable": "No data available in table", - /** - * You can control the default ordering direction, and even alter the - * behaviour of the sort handler (i.e. only allow ascending ordering etc) - * using this parameter. - * @type array - * @default [ 'asc', 'desc' ] - * - * @name DataTable.defaults.column.orderSequence - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "orderSequence": [ "asc" ], "targets": [ 1 ] }, - * { "orderSequence": [ "desc", "asc", "asc" ], "targets": [ 2 ] }, - * { "orderSequence": [ "desc" ], "targets": [ 3 ] } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * null, - * { "orderSequence": [ "asc" ] }, - * { "orderSequence": [ "desc", "asc", "asc" ] }, - * { "orderSequence": [ "desc" ] }, - * null - * ] - * } ); - * } ); - */ - "asSorting": [ 'asc', 'desc' ], + /** + * This string gives information to the end user about the information + * that is current on display on the page. The following tokens can be + * used in the string and will be dynamically replaced as the table + * display updates. This tokens can be placed anywhere in the string, or + * removed as needed by the language requires: + * + * * `\_START\_` - Display index of the first record on the current page + * * `\_END\_` - Display index of the last record on the current page + * * `\_TOTAL\_` - Number of records in the table after filtering + * * `\_MAX\_` - Number of records in the table without filtering + * * `\_PAGE\_` - Current page number + * * `\_PAGES\_` - Total number of pages of data in the table + */ + "sInfo": "Showing _START_ to _END_ of _TOTAL_ _ENTRIES-TOTAL_", + + + /** + * Display information string for when the table is empty. Typically the + * format of this string should match `info`. + */ + "sInfoEmpty": "Showing 0 to 0 of 0 _ENTRIES-TOTAL_", + + + /** + * When a user filters the information in a table, this string is appended + * to the information (`info`) to give an idea of how strong the filtering + * is. The variable _MAX_ is dynamically updated. + */ + "sInfoFiltered": "(filtered from _MAX_ total _ENTRIES-MAX_)", + + + /** + * If can be useful to append extra information to the info string at times, + * and this variable does exactly that. This information will be appended to + * the `info` (`infoEmpty` and `infoFiltered` in whatever combination they are + * being used) at all times. + */ + "sInfoPostFix": "", + + + /** + * This decimal place operator is a little different from the other + * language options since DataTables doesn't output floating point + * numbers, so it won't ever use this for display of a number. Rather, + * what this parameter does is modify the sort methods of the table so + * that numbers which are in a format which has a character other than + * a period (`.`) as a decimal place will be sorted numerically. + * + * Note that numbers with different decimal places cannot be shown in + * the same table and still be sortable, the table must be consistent. + * However, multiple different tables on the page can use different + * decimal place characters. + */ + "sDecimal": "", - /** - * Enable or disable filtering on the data in this column. - * @type boolean - * @default true - * - * @name DataTable.defaults.column.searchable - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "searchable": false, "targets": [ 0 ] } - * ] } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "searchable": false }, - * null, - * null, - * null, - * null - * ] } ); - * } ); - */ - "bSearchable": true, + /** + * DataTables has a build in number formatter (`formatNumber`) which is + * used to format large numbers that are used in the table information. + * By default a comma is used, but this can be trivially changed to any + * character you wish with this parameter. + */ + "sThousands": ",", - /** - * Enable or disable ordering on this column. - * @type boolean - * @default true - * - * @name DataTable.defaults.column.orderable - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "orderable": false, "targets": [ 0 ] } - * ] } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "orderable": false }, - * null, - * null, - * null, - * null - * ] } ); - * } ); - */ - "bSortable": true, + /** + * Detail the action that will be taken when the drop down menu for the + * pagination length option is changed. The '_MENU_' variable is replaced + * with a default select list of 10, 25, 50 and 100, and can be replaced + * with a custom select box if required. + */ + "sLengthMenu": "_MENU_ _ENTRIES_ per page", - /** - * Enable or disable the display of this column. - * @type boolean - * @default true - * - * @name DataTable.defaults.column.visible - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "visible": false, "targets": [ 0 ] } - * ] } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "visible": false }, - * null, - * null, - * null, - * null - * ] } ); - * } ); - */ - "bVisible": true, + /** + * When using Ajax sourced data and during the first draw when DataTables is + * gathering the data, this message is shown in an empty row in the table to + * indicate to the end user the the data is being loaded. Note that this + * parameter is not used when loading data by server-side processing, just + * Ajax sourced data with client-side processing. + */ + "sLoadingRecords": "Loading...", - /** - * Developer definable function that is called whenever a cell is created (Ajax source, - * etc) or processed for input (DOM source). This can be used as a compliment to mRender - * allowing you to modify the DOM element (add background colour for example) when the - * element is available. - * @type function - * @param {element} td The TD node that has been created - * @param {*} cellData The Data for the cell - * @param {array|object} rowData The data for the whole row - * @param {int} row The row index for the aoData data store - * @param {int} col The column index for aoColumns - * - * @name DataTable.defaults.column.createdCell - * @dtopt Columns - * - * @example - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [3], - * "createdCell": function (td, cellData, rowData, row, col) { - * if ( cellData == "1.7" ) { - * $(td).css('color', 'blue') - * } - * } - * } ] - * }); - * } ); - */ - "fnCreatedCell": null, + /** + * Text which is displayed when the table is processing a user action + * (usually a sort command or similar). + */ + "sProcessing": "", - /** - * This parameter has been replaced by `data` in DataTables to ensure naming - * consistency. `dataProp` can still be used, as there is backwards - * compatibility in DataTables for this option, but it is strongly - * recommended that you use `data` in preference to `dataProp`. - * @name DataTable.defaults.column.dataProp - */ + /** + * Details the actions that will be taken when the user types into the + * filtering input text box. The variable "_INPUT_", if used in the string, + * is replaced with the HTML text box for the filtering input allowing + * control over where it appears in the string. If "_INPUT_" is not given + * then the input box is appended to the string automatically. + */ + "sSearch": "Search:", - /** - * This property can be used to read data from any data source property, - * including deeply nested objects / properties. `data` can be given in a - * number of different ways which effect its behaviour: - * - * * `integer` - treated as an array index for the data source. This is the - * default that DataTables uses (incrementally increased for each column). - * * `string` - read an object property from the data source. There are - * three 'special' options that can be used in the string to alter how - * DataTables reads the data from the source object: - * * `.` - Dotted Javascript notation. Just as you use a `.` in - * Javascript to read from nested objects, so to can the options - * specified in `data`. For example: `browser.version` or - * `browser.name`. If your object parameter name contains a period, use - * `\\` to escape it - i.e. `first\\.name`. - * * `[]` - Array notation. DataTables can automatically combine data - * from and array source, joining the data with the characters provided - * between the two brackets. For example: `name[, ]` would provide a - * comma-space separated list from the source array. If no characters - * are provided between the brackets, the original array source is - * returned. - * * `()` - Function notation. Adding `()` to the end of a parameter will - * execute a function of the name given. For example: `browser()` for a - * simple function on the data source, `browser.version()` for a - * function in a nested property or even `browser().version` to get an - * object property if the function called returns an object. Note that - * function notation is recommended for use in `render` rather than - * `data` as it is much simpler to use as a renderer. - * * `null` - use the original data source for the row rather than plucking - * data directly from it. This action has effects on two other - * initialisation options: - * * `defaultContent` - When null is given as the `data` option and - * `defaultContent` is specified for the column, the value defined by - * `defaultContent` will be used for the cell. - * * `render` - When null is used for the `data` option and the `render` - * option is specified for the column, the whole data source for the - * row is used for the renderer. - * * `function` - the function given will be executed whenever DataTables - * needs to set or get the data for a cell in the column. The function - * takes three parameters: - * * Parameters: - * * `{array|object}` The data source for the row - * * `{string}` The type call data requested - this will be 'set' when - * setting data or 'filter', 'display', 'type', 'sort' or undefined - * when gathering data. Note that when `undefined` is given for the - * type DataTables expects to get the raw data for the object back< - * * `{*}` Data to set when the second parameter is 'set'. - * * Return: - * * The return value from the function is not required when 'set' is - * the type of call, but otherwise the return is what will be used - * for the data requested. - * - * Note that `data` is a getter and setter option. If you just require - * formatting of data for output, you will likely want to use `render` which - * is simply a getter and thus simpler to use. - * - * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The - * name change reflects the flexibility of this property and is consistent - * with the naming of mRender. If 'mDataProp' is given, then it will still - * be used by DataTables, as it automatically maps the old name to the new - * if required. - * - * @type string|int|function|null - * @default null <i>Use automatically calculated column index</i> - * - * @name DataTable.defaults.column.data - * @dtopt Columns - * - * @example - * // Read table data from objects - * // JSON structure for each row: - * // { - * // "engine": {value}, - * // "browser": {value}, - * // "platform": {value}, - * // "version": {value}, - * // "grade": {value} - * // } - * $(document).ready( function() { - * $('#example').dataTable( { - * "ajaxSource": "sources/objects.txt", - * "columns": [ - * { "data": "engine" }, - * { "data": "browser" }, - * { "data": "platform" }, - * { "data": "version" }, - * { "data": "grade" } - * ] - * } ); - * } ); - * - * @example - * // Read information from deeply nested objects - * // JSON structure for each row: - * // { - * // "engine": {value}, - * // "browser": {value}, - * // "platform": { - * // "inner": {value} - * // }, - * // "details": [ - * // {value}, {value} - * // ] - * // } - * $(document).ready( function() { - * $('#example').dataTable( { - * "ajaxSource": "sources/deep.txt", - * "columns": [ - * { "data": "engine" }, - * { "data": "browser" }, - * { "data": "platform.inner" }, - * { "data": "details.0" }, - * { "data": "details.1" } - * ] - * } ); - * } ); - * - * @example - * // Using `data` as a function to provide different information for - * // sorting, filtering and display. In this case, currency (price) - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [ 0 ], - * "data": function ( source, type, val ) { - * if (type === 'set') { - * source.price = val; - * // Store the computed display and filter values for efficiency - * source.price_display = val=="" ? "" : "$"+numberFormat(val); - * source.price_filter = val=="" ? "" : "$"+numberFormat(val)+" "+val; - * return; - * } - * else if (type === 'display') { - * return source.price_display; - * } - * else if (type === 'filter') { - * return source.price_filter; - * } - * // 'sort', 'type' and undefined all just use the integer - * return source.price; - * } - * } ] - * } ); - * } ); - * - * @example - * // Using default content - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [ 0 ], - * "data": null, - * "defaultContent": "Click to edit" - * } ] - * } ); - * } ); - * - * @example - * // Using array notation - outputting a list from an array - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [ 0 ], - * "data": "name[, ]" - * } ] - * } ); - * } ); - * - */ - "mData": null, + /** + * Assign a `placeholder` attribute to the search `input` element + * @type string + * @default + * + * @dtopt Language + * @name DataTable.defaults.language.searchPlaceholder + */ + "sSearchPlaceholder": "", - /** - * This property is the rendering partner to `data` and it is suggested that - * when you want to manipulate data for display (including filtering, - * sorting etc) without altering the underlying data for the table, use this - * property. `render` can be considered to be the the read only companion to - * `data` which is read / write (then as such more complex). Like `data` - * this option can be given in a number of different ways to effect its - * behaviour: - * - * * `integer` - treated as an array index for the data source. This is the - * default that DataTables uses (incrementally increased for each column). - * * `string` - read an object property from the data source. There are - * three 'special' options that can be used in the string to alter how - * DataTables reads the data from the source object: - * * `.` - Dotted Javascript notation. Just as you use a `.` in - * Javascript to read from nested objects, so to can the options - * specified in `data`. For example: `browser.version` or - * `browser.name`. If your object parameter name contains a period, use - * `\\` to escape it - i.e. `first\\.name`. - * * `[]` - Array notation. DataTables can automatically combine data - * from and array source, joining the data with the characters provided - * between the two brackets. For example: `name[, ]` would provide a - * comma-space separated list from the source array. If no characters - * are provided between the brackets, the original array source is - * returned. - * * `()` - Function notation. Adding `()` to the end of a parameter will - * execute a function of the name given. For example: `browser()` for a - * simple function on the data source, `browser.version()` for a - * function in a nested property or even `browser().version` to get an - * object property if the function called returns an object. - * * `object` - use different data for the different data types requested by - * DataTables ('filter', 'display', 'type' or 'sort'). The property names - * of the object is the data type the property refers to and the value can - * defined using an integer, string or function using the same rules as - * `render` normally does. Note that an `_` option _must_ be specified. - * This is the default value to use if you haven't specified a value for - * the data type requested by DataTables. - * * `function` - the function given will be executed whenever DataTables - * needs to set or get the data for a cell in the column. The function - * takes three parameters: - * * Parameters: - * * {array|object} The data source for the row (based on `data`) - * * {string} The type call data requested - this will be 'filter', - * 'display', 'type' or 'sort'. - * * {array|object} The full data source for the row (not based on - * `data`) - * * Return: - * * The return value from the function is what will be used for the - * data requested. - * - * @type string|int|function|object|null - * @default null Use the data source value. - * - * @name DataTable.defaults.column.render - * @dtopt Columns - * - * @example - * // Create a comma separated list from an array of objects - * $(document).ready( function() { - * $('#example').dataTable( { - * "ajaxSource": "sources/deep.txt", - * "columns": [ - * { "data": "engine" }, - * { "data": "browser" }, - * { - * "data": "platform", - * "render": "[, ].name" - * } - * ] - * } ); - * } ); - * - * @example - * // Execute a function to obtain data - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [ 0 ], - * "data": null, // Use the full data source object for the renderer's source - * "render": "browserName()" - * } ] - * } ); - * } ); - * - * @example - * // As an object, extracting different data for the different types - * // This would be used with a data source such as: - * // { "phone": 5552368, "phone_filter": "5552368 555-2368", "phone_display": "555-2368" } - * // Here the `phone` integer is used for sorting and type detection, while `phone_filter` - * // (which has both forms) is used for filtering for if a user inputs either format, while - * // the formatted phone number is the one that is shown in the table. - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [ 0 ], - * "data": null, // Use the full data source object for the renderer's source - * "render": { - * "_": "phone", - * "filter": "phone_filter", - * "display": "phone_display" - * } - * } ] - * } ); - * } ); - * - * @example - * // Use as a function to create a link from the data source - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [ 0 ], - * "data": "download_link", - * "render": function ( data, type, full ) { - * return '<a href="'+data+'">Download</a>'; - * } - * } ] - * } ); - * } ); - */ - "mRender": null, + /** + * All of the language information can be stored in a file on the + * server-side, which DataTables will look up if this parameter is passed. + * It must store the URL of the language file, which is in a JSON format, + * and the object has the same properties as the oLanguage object in the + * initialiser object (i.e. the above parameters). Please refer to one of + * the example language files to see how this works in action. + */ + "sUrl": "", - /** - * Change the cell type created for the column - either TD cells or TH cells. This - * can be useful as TH cells have semantic meaning in the table body, allowing them - * to act as a header for a row (you may wish to add scope='row' to the TH elements). - * @type string - * @default td - * - * @name DataTable.defaults.column.cellType - * @dtopt Columns - * - * @example - * // Make the first column use TH cells - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ { - * "targets": [ 0 ], - * "cellType": "th" - * } ] - * } ); - * } ); - */ - "sCellType": "td", + /** + * Text shown inside the table records when the is no information to be + * displayed after filtering. `emptyTable` is shown when there is simply no + * information in the table at all (regardless of filtering). + */ + "sZeroRecords": "No matching records found" + }, /** - * Class to give to each cell in this column. - * @type string - * @default <i>Empty string</i> - * - * @name DataTable.defaults.column.class - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "class": "my_class", "targets": [ 0 ] } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "class": "my_class" }, - * null, - * null, - * null, - * null - * ] - * } ); - * } ); + * This parameter allows you to have define the global filtering state at + * initialisation time. As an object the `search` parameter must be + * defined, but all other parameters are optional. When `regex` is true, + * the search string will be treated as a regular expression, when false + * (default) it will be treated as a straight string. When `smart` + * DataTables will use it's smart filtering methods (to word match at + * any point in the data), when false this will not be done. */ - "sClass": "", + "oSearch": $.extend( {}, DataTable.models.oSearch ), + /** - * When DataTables calculates the column widths to assign to each column, - * it finds the longest string in each column and then constructs a - * temporary table and reads the widths from that. The problem with this - * is that "mmm" is much wider then "iiii", but the latter is a longer - * string - thus the calculation can go wrong (doing it properly and putting - * it into an DOM object and measuring that is horribly(!) slow). Thus as - * a "work around" we provide this option. It will append its value to the - * text that is found to be the longest string for the column - i.e. padding. - * Generally you shouldn't need this! - * @type string - * @default <i>Empty string<i> - * - * @name DataTable.defaults.column.contentPadding - * @dtopt Columns - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * null, - * null, - * null, - * { - * "contentPadding": "mmm" - * } - * ] - * } ); - * } ); + * Table and control layout. This replaces the legacy `dom` option. */ - "sContentPadding": "", + layout: { + topStart: 'pageLength', + topEnd: 'search', + bottomStart: 'info', + bottomEnd: 'paging' + }, /** - * Allows a default value to be given for a column's data, and will be used - * whenever a null data source is encountered (this can be because `data` - * is set to null, or because the data source itself is null). - * @type string - * @default null - * - * @name DataTable.defaults.column.defaultContent - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { - * "data": null, - * "defaultContent": "Edit", - * "targets": [ -1 ] - * } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * null, - * null, - * null, - * { - * "data": null, - * "defaultContent": "Edit" - * } - * ] - * } ); - * } ); + * Legacy DOM layout option */ - "sDefaultContent": null, + "sDom": null, /** - * This parameter is only used in DataTables' server-side processing. It can - * be exceptionally useful to know what columns are being displayed on the - * client side, and to map these to database fields. When defined, the names - * also allow DataTables to reorder information from the server if it comes - * back in an unexpected order (i.e. if you switch your columns around on the - * client-side, your server-side code does not also need updating). - * @type string - * @default <i>Empty string</i> - * - * @name DataTable.defaults.column.name - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "name": "engine", "targets": [ 0 ] }, - * { "name": "browser", "targets": [ 1 ] }, - * { "name": "platform", "targets": [ 2 ] }, - * { "name": "version", "targets": [ 3 ] }, - * { "name": "grade", "targets": [ 4 ] } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "name": "engine" }, - * { "name": "browser" }, - * { "name": "platform" }, - * { "name": "version" }, - * { "name": "grade" } - * ] - * } ); - * } ); + * Search delay option. This will throttle full table searches that use the + * DataTables provided search input element (it does not effect calls to + * `dt-api search()`, providing a delay before the search is made. */ - "sName": "", + "searchDelay": null, /** - * Defines a data source type for the ordering which can be used to read - * real-time information from the table (updating the internally cached - * version) prior to ordering. This allows ordering to occur on user - * editable elements such as form inputs. - * @type string - * @default std - * - * @name DataTable.defaults.column.orderDataType - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "orderDataType": "dom-text", "targets": [ 2, 3 ] }, - * { "type": "numeric", "targets": [ 3 ] }, - * { "orderDataType": "dom-select", "targets": [ 4 ] }, - * { "orderDataType": "dom-checkbox", "targets": [ 5 ] } - * ] - * } ); - * } ); + * DataTables features six different built-in options for the buttons to + * display for pagination control: * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * null, - * null, - * { "orderDataType": "dom-text" }, - * { "orderDataType": "dom-text", "type": "numeric" }, - * { "orderDataType": "dom-select" }, - * { "orderDataType": "dom-checkbox" } - * ] - * } ); - * } ); + * * `numbers` - Page number buttons only + * * `simple` - 'Previous' and 'Next' buttons only + * * 'simple_numbers` - 'Previous' and 'Next' buttons, plus page numbers + * * `full` - 'First', 'Previous', 'Next' and 'Last' buttons + * * `full_numbers` - 'First', 'Previous', 'Next' and 'Last' buttons, plus page numbers + * * `first_last_numbers` - 'First' and 'Last' buttons, plus page numbers */ - "sSortDataType": "std", + "sPaginationType": "full_numbers", /** - * The title of this column. - * @type string - * @default null <i>Derived from the 'TH' value for this column in the - * original HTML table.</i> - * - * @name DataTable.defaults.column.title - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "title": "My column title", "targets": [ 0 ] } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "title": "My column title" }, - * null, - * null, - * null, - * null - * ] - * } ); - * } ); + * Enable horizontal scrolling. When a table is too wide to fit into a + * certain layout, or you have a large number of columns in the table, you + * can enable x-scrolling to show the table in a viewport, which can be + * scrolled. This property can be `true` which will allow the table to + * scroll horizontally when needed, or any CSS unit, or a number (in which + * case it will be treated as a pixel measurement). Setting as simply `true` + * is recommended. */ - "sTitle": null, + "sScrollX": "", /** - * The type allows you to specify how the data for this column will be - * ordered. Four types (string, numeric, date and html (which will strip - * HTML tags before ordering)) are currently available. Note that only date - * formats understood by Javascript's Date() object will be accepted as type - * date. For example: "Mar 26, 2008 5:03 PM". May take the values: 'string', - * 'numeric', 'date' or 'html' (by default). Further types can be adding - * through plug-ins. - * @type string - * @default null <i>Auto-detected from raw data</i> - * - * @name DataTable.defaults.column.type - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "type": "html", "targets": [ 0 ] } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "type": "html" }, - * null, - * null, - * null, - * null - * ] - * } ); - * } ); + * This property can be used to force a DataTable to use more width than it + * might otherwise do when x-scrolling is enabled. For example if you have a + * table which requires to be well spaced, this parameter is useful for + * "over-sizing" the table, and thus forcing scrolling. This property can by + * any CSS unit, or a number (in which case it will be treated as a pixel + * measurement). */ - "sType": null, + "sScrollXInner": "", /** - * Defining the width of the column, this parameter may take any CSS value - * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not - * been given a specific width through this interface ensuring that the table - * remains readable. - * @type string - * @default null <i>Automatic</i> - * - * @name DataTable.defaults.column.width - * @dtopt Columns - * - * @example - * // Using `columnDefs` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columnDefs": [ - * { "width": "20%", "targets": [ 0 ] } - * ] - * } ); - * } ); - * - * @example - * // Using `columns` - * $(document).ready( function() { - * $('#example').dataTable( { - * "columns": [ - * { "width": "20%" }, - * null, - * null, - * null, - * null - * ] - * } ); - * } ); + * Enable vertical scrolling. Vertical scrolling will constrain the DataTable + * to the given height, and enable scrolling for any data which overflows the + * current viewport. This can be used as an alternative to paging to display + * a lot of data in a small area (although paging and scrolling can both be + * enabled at the same time). This property can be any CSS unit, or a number + * (in which case it will be treated as a pixel measurement). */ - "sWidth": null - }; - - _fnHungarianMap( DataTable.defaults.column ); - + "sScrollY": "", - /** - * DataTables settings object - this holds all the information needed for a - * given table, including configuration, data and current application of the - * table options. DataTables does not have a single instance for each DataTable - * with the settings attached to that instance, but rather instances of the - * DataTable "class" are created on-the-fly as needed (typically by a - * $().dataTable() call) and the settings object is then applied to that - * instance. - * - * Note that this object is related to {@link DataTable.defaults} but this - * one is the internal data store for DataTables's cache of columns. It should - * NOT be manipulated outside of DataTables. Any configuration should be done - * through the initialisation options. - * @namespace - * @todo Really should attach the settings object to individual instances so we - * don't need to create new instances on each $().dataTable() call (if the - * table already exists). It would also save passing oSettings around and - * into every single function. However, this is a very significant - * architecture change for DataTables and will almost certainly break - * backwards compatibility with older installations. This is something that - * will be done in 2.0. - */ - DataTable.models.oSettings = { /** - * Primary features of DataTables and their enablement state. - * @namespace + * __Deprecated__ The functionality provided by this parameter has now been + * superseded by that provided through `ajax`, which should be used instead. + * + * Set the HTTP method that is used to make the Ajax call for server-side + * processing or Ajax sourced data. */ - "oFeatures": { - - /** - * Flag to say if DataTables should automatically try to calculate the - * optimum table and columns widths (true) or not (false). - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bAutoWidth": null, - - /** - * Delay the creation of TR and TD elements until they are actually - * needed by a driven page draw. This can give a significant speed - * increase for Ajax source and Javascript source data, but makes no - * difference at all for DOM and server-side processing tables. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bDeferRender": null, - - /** - * Enable filtering on the table or not. Note that if this is disabled - * then there is no filtering at all on the table, including fnFilter. - * To just remove the filtering input use sDom and remove the 'f' option. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bFilter": null, - - /** - * Table information element (the 'Showing x of y records' div) enable - * flag. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bInfo": null, - - /** - * Present a user control allowing the end user to change the page size - * when pagination is enabled. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bLengthChange": null, - - /** - * Pagination enabled or not. Note that if this is disabled then length - * changing must also be disabled. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bPaginate": null, + "sServerMethod": "GET", - /** - * Processing indicator enable flag whenever DataTables is enacting a - * user request - typically an Ajax request for server-side processing. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bProcessing": null, - /** - * Server-side processing enabled flag - when enabled DataTables will - * get all data from the server for every draw - there is no filtering, - * sorting or paging done on the client-side. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bServerSide": null, + /** + * DataTables makes use of renderers when displaying HTML elements for + * a table. These renderers can be added or modified by plug-ins to + * generate suitable mark-up for a site. For example the Bootstrap + * integration plug-in for DataTables uses a paging button renderer to + * display pagination buttons in the mark-up required by Bootstrap. + * + * For further information about the renderers available see + * DataTable.ext.renderer + */ + "renderer": null, - /** - * Sorting enablement flag. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bSort": null, - /** - * Multi-column sorting - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bSortMulti": null, + /** + * Set the data property name that DataTables should use to get a row's id + * to set as the `id` property in the node. + */ + "rowId": "DT_RowId", - /** - * Apply a class to the columns which are being sorted to provide a - * visual highlight or not. This can slow things down when enabled since - * there is a lot of DOM interaction. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bSortClasses": null, - /** - * State saving enablement flag. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bStateSave": null - }, + /** + * Caption value + */ + "caption": null + }; + + _fnHungarianMap( DataTable.defaults ); + + /* + * Developer note - See note in model.defaults.js about the use of Hungarian + * notation and camel case. + */ + + /** + * Column options that can be given to DataTables at initialisation time. + * @namespace + */ + DataTable.defaults.column = { /** - * Scrolling settings for a table. - * @namespace + * Define which column(s) an order will occur on for this column. This + * allows a column's ordering to take multiple columns into account when + * doing a sort or use the data from a different column. For example first + * name / last name columns make sense to do a multi-column sort over the + * two columns. */ - "oScroll": { - /** - * When the table is shorter in height than sScrollY, collapse the - * table container down to the height of the table (when true). - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - */ - "bCollapse": null, + "aDataSort": null, + "iDataSort": -1, - /** - * Width of the scrollbar for the web-browser's platform. Calculated - * during table initialisation. - * @type int - * @default 0 - */ - "iBarWidth": 0, + ariaTitle: '', - /** - * Viewport width for horizontal scrolling. Horizontal scrolling is - * disabled if an empty string. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type string - */ - "sX": null, - /** - * Width to expand the table to when using x-scrolling. Typically you - * should not need to use this. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type string - * @deprecated - */ - "sXInner": null, + /** + * You can control the default ordering direction, and even alter the + * behaviour of the sort handler (i.e. only allow ascending ordering etc) + * using this parameter. + */ + "asSorting": [ 'asc', 'desc', '' ], - /** - * Viewport height for vertical scrolling. Vertical scrolling is disabled - * if an empty string. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type string - */ - "sY": null - }, /** - * Language information for the table. - * @namespace - * @extends DataTable.defaults.oLanguage + * Enable or disable filtering on the data in this column. */ - "oLanguage": { - /** - * Information callback function. See - * {@link DataTable.defaults.fnInfoCallback} - * @type function - * @default null - */ - "fnInfoCallback": null - }, + "bSearchable": true, + /** - * Browser support parameters - * @namespace + * Enable or disable ordering on this column. */ - "oBrowser": { - /** - * Indicate if the browser incorrectly calculates width:100% inside a - * scrolling element (IE6/7) - * @type boolean - * @default false - */ - "bScrollOversize": false, + "bSortable": true, - /** - * Determine if the vertical scrollbar is on the right or left of the - * scrolling container - needed for rtl language layout, although not - * all browsers move the scrollbar (Safari). - * @type boolean - * @default false - */ - "bScrollbarLeft": false, - /** - * Flag for if `getBoundingClientRect` is fully supported or not - * @type boolean - * @default false - */ - "bBounding": false, + /** + * Enable or disable the display of this column. + */ + "bVisible": true, - /** - * Browser scrollbar width - * @type integer - * @default 0 - */ - "barWidth": 0 - }, + + /** + * Developer definable function that is called whenever a cell is created (Ajax source, + * etc) or processed for input (DOM source). This can be used as a compliment to mRender + * allowing you to modify the DOM element (add background colour for example) when the + * element is available. + */ + "fnCreatedCell": null, - "ajax": null, + /** + * This property can be used to read data from any data source property, + * including deeply nested objects / properties. `data` can be given in a + * number of different ways which effect its behaviour: + * + * * `integer` - treated as an array index for the data source. This is the + * default that DataTables uses (incrementally increased for each column). + * * `string` - read an object property from the data source. There are + * three 'special' options that can be used in the string to alter how + * DataTables reads the data from the source object: + * * `.` - Dotted Javascript notation. Just as you use a `.` in + * Javascript to read from nested objects, so to can the options + * specified in `data`. For example: `browser.version` or + * `browser.name`. If your object parameter name contains a period, use + * `\\` to escape it - i.e. `first\\.name`. + * * `[]` - Array notation. DataTables can automatically combine data + * from and array source, joining the data with the characters provided + * between the two brackets. For example: `name[, ]` would provide a + * comma-space separated list from the source array. If no characters + * are provided between the brackets, the original array source is + * returned. + * * `()` - Function notation. Adding `()` to the end of a parameter will + * execute a function of the name given. For example: `browser()` for a + * simple function on the data source, `browser.version()` for a + * function in a nested property or even `browser().version` to get an + * object property if the function called returns an object. Note that + * function notation is recommended for use in `render` rather than + * `data` as it is much simpler to use as a renderer. + * * `null` - use the original data source for the row rather than plucking + * data directly from it. This action has effects on two other + * initialisation options: + * * `defaultContent` - When null is given as the `data` option and + * `defaultContent` is specified for the column, the value defined by + * `defaultContent` will be used for the cell. + * * `render` - When null is used for the `data` option and the `render` + * option is specified for the column, the whole data source for the + * row is used for the renderer. + * * `function` - the function given will be executed whenever DataTables + * needs to set or get the data for a cell in the column. The function + * takes three parameters: + * * Parameters: + * * `{array|object}` The data source for the row + * * `{string}` The type call data requested - this will be 'set' when + * setting data or 'filter', 'display', 'type', 'sort' or undefined + * when gathering data. Note that when `undefined` is given for the + * type DataTables expects to get the raw data for the object back< + * * `{*}` Data to set when the second parameter is 'set'. + * * Return: + * * The return value from the function is not required when 'set' is + * the type of call, but otherwise the return is what will be used + * for the data requested. + * + * Note that `data` is a getter and setter option. If you just require + * formatting of data for output, you will likely want to use `render` which + * is simply a getter and thus simpler to use. + * + * Note that prior to DataTables 1.9.2 `data` was called `mDataProp`. The + * name change reflects the flexibility of this property and is consistent + * with the naming of mRender. If 'mDataProp' is given, then it will still + * be used by DataTables, as it automatically maps the old name to the new + * if required. + */ + "mData": null, + + + /** + * This property is the rendering partner to `data` and it is suggested that + * when you want to manipulate data for display (including filtering, + * sorting etc) without altering the underlying data for the table, use this + * property. `render` can be considered to be the the read only companion to + * `data` which is read / write (then as such more complex). Like `data` + * this option can be given in a number of different ways to effect its + * behaviour: + * + * * `integer` - treated as an array index for the data source. This is the + * default that DataTables uses (incrementally increased for each column). + * * `string` - read an object property from the data source. There are + * three 'special' options that can be used in the string to alter how + * DataTables reads the data from the source object: + * * `.` - Dotted Javascript notation. Just as you use a `.` in + * Javascript to read from nested objects, so to can the options + * specified in `data`. For example: `browser.version` or + * `browser.name`. If your object parameter name contains a period, use + * `\\` to escape it - i.e. `first\\.name`. + * * `[]` - Array notation. DataTables can automatically combine data + * from and array source, joining the data with the characters provided + * between the two brackets. For example: `name[, ]` would provide a + * comma-space separated list from the source array. If no characters + * are provided between the brackets, the original array source is + * returned. + * * `()` - Function notation. Adding `()` to the end of a parameter will + * execute a function of the name given. For example: `browser()` for a + * simple function on the data source, `browser.version()` for a + * function in a nested property or even `browser().version` to get an + * object property if the function called returns an object. + * * `object` - use different data for the different data types requested by + * DataTables ('filter', 'display', 'type' or 'sort'). The property names + * of the object is the data type the property refers to and the value can + * defined using an integer, string or function using the same rules as + * `render` normally does. Note that an `_` option _must_ be specified. + * This is the default value to use if you haven't specified a value for + * the data type requested by DataTables. + * * `function` - the function given will be executed whenever DataTables + * needs to set or get the data for a cell in the column. The function + * takes three parameters: + * * Parameters: + * * {array|object} The data source for the row (based on `data`) + * * {string} The type call data requested - this will be 'filter', + * 'display', 'type' or 'sort'. + * * {array|object} The full data source for the row (not based on + * `data`) + * * Return: + * * The return value from the function is what will be used for the + * data requested. + */ + "mRender": null, /** - * Array referencing the nodes which are used for the features. The - * parameters of this object match what is allowed by sDom - i.e. - * <ul> - * <li>'l' - Length changing</li> - * <li>'f' - Filtering input</li> - * <li>'t' - The table!</li> - * <li>'i' - Information</li> - * <li>'p' - Pagination</li> - * <li>'r' - pRocessing</li> - * </ul> - * @type array - * @default [] + * Change the cell type created for the column - either TD cells or TH cells. This + * can be useful as TH cells have semantic meaning in the table body, allowing them + * to act as a header for a row (you may wish to add scope='row' to the TH elements). */ - "aanFeatures": [], + "sCellType": "td", - /** - * Store data information - see {@link DataTable.models.oRow} for detailed - * information. - * @type array - * @default [] - */ - "aoData": [], /** - * Array of indexes which are in the current display (after filtering etc) - * @type array - * @default [] + * Class to give to each cell in this column. */ - "aiDisplay": [], + "sClass": "", /** - * Array of indexes for display - no filtering - * @type array - * @default [] + * When DataTables calculates the column widths to assign to each column, + * it finds the longest string in each column and then constructs a + * temporary table and reads the widths from that. The problem with this + * is that "mmm" is much wider then "iiii", but the latter is a longer + * string - thus the calculation can go wrong (doing it properly and putting + * it into an DOM object and measuring that is horribly(!) slow). Thus as + * a "work around" we provide this option. It will append its value to the + * text that is found to be the longest string for the column - i.e. padding. + * Generally you shouldn't need this! */ - "aiDisplayMaster": [], + "sContentPadding": "", - /** - * Map of row ids to data indexes - * @type object - * @default {} - */ - "aIds": {}, /** - * Store information about each column that is in use - * @type array - * @default [] + * Allows a default value to be given for a column's data, and will be used + * whenever a null data source is encountered (this can be because `data` + * is set to null, or because the data source itself is null). */ - "aoColumns": [], + "sDefaultContent": null, - /** - * Store information about the table's header - * @type array - * @default [] - */ - "aoHeader": [], /** - * Store information about the table's footer - * @type array - * @default [] + * This parameter is only used in DataTables' server-side processing. It can + * be exceptionally useful to know what columns are being displayed on the + * client side, and to map these to database fields. When defined, the names + * also allow DataTables to reorder information from the server if it comes + * back in an unexpected order (i.e. if you switch your columns around on the + * client-side, your server-side code does not also need updating). */ - "aoFooter": [], + "sName": "", - /** - * Store the applied global search information in case we want to force a - * research or compare the old search to a new one. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @namespace - * @extends DataTable.models.oSearch - */ - "oPreviousSearch": {}, /** - * Store the applied search for each column - see - * {@link DataTable.models.oSearch} for the format that is used for the - * filtering information for each column. - * @type array - * @default [] + * Defines a data source type for the ordering which can be used to read + * real-time information from the table (updating the internally cached + * version) prior to ordering. This allows ordering to occur on user + * editable elements such as form inputs. */ - "aoPreSearchCols": [], + "sSortDataType": "std", - /** - * Sorting that is applied to the table. Note that the inner arrays are - * used in the following manner: - * <ul> - * <li>Index 0 - column number</li> - * <li>Index 1 - current sorting direction</li> - * </ul> - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type array - * @todo These inner arrays should really be objects - */ - "aaSorting": null, /** - * Sorting that is always applied to the table (i.e. prefixed in front of - * aaSorting). - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type array - * @default [] + * The title of this column. */ - "aaSortingFixed": [], + "sTitle": null, - /** - * Classes to use for the striping of a table. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type array - * @default [] - */ - "asStripeClasses": null, /** - * If restoring a table - we should restore its striping classes as well - * @type array - * @default [] + * The type allows you to specify how the data for this column will be + * ordered. Four types (string, numeric, date and html (which will strip + * HTML tags before ordering)) are currently available. Note that only date + * formats understood by Javascript's Date() object will be accepted as type + * date. For example: "Mar 26, 2008 5:03 PM". May take the values: 'string', + * 'numeric', 'date' or 'html' (by default). Further types can be adding + * through plug-ins. */ - "asDestroyStripes": [], + "sType": null, - /** - * If restoring a table - we should restore its width - * @type int - * @default 0 - */ - "sDestroyWidth": 0, /** - * Callback functions array for every time a row is inserted (i.e. on a draw). - * @type array - * @default [] + * Defining the width of the column, this parameter may take any CSS value + * (3em, 20px etc). DataTables applies 'smart' widths to columns which have not + * been given a specific width through this interface ensuring that the table + * remains readable. */ - "aoRowCallback": [], + "sWidth": null + }; - /** - * Callback functions for the header on each draw. - * @type array - * @default [] - */ - "aoHeaderCallback": [], + _fnHungarianMap( DataTable.defaults.column ); - /** - * Callback function for the footer on each draw. - * @type array - * @default [] - */ - "aoFooterCallback": [], - /** - * Array of callback functions for draw callback functions - * @type array - * @default [] - */ - "aoDrawCallback": [], + /** + * DataTables settings object - this holds all the information needed for a + * given table, including configuration, data and current application of the + * table options. DataTables does not have a single instance for each DataTable + * with the settings attached to that instance, but rather instances of the + * DataTable "class" are created on-the-fly as needed (typically by a + * $().dataTable() call) and the settings object is then applied to that + * instance. + * + * Note that this object is related to {@link DataTable.defaults} but this + * one is the internal data store for DataTables's cache of columns. It should + * NOT be manipulated outside of DataTables. Any configuration should be done + * through the initialisation options. + */ + DataTable.models.oSettings = { /** - * Array of callback functions for row created function - * @type array - * @default [] + * Primary features of DataTables and their enablement state. */ - "aoRowCreatedCallback": [], + "oFeatures": { - /** - * Callback functions for just before the table is redrawn. A return of - * false will be used to cancel the draw. - * @type array - * @default [] - */ - "aoPreDrawCallback": [], + /** + * Flag to say if DataTables should automatically try to calculate the + * optimum table and columns widths (true) or not (false). + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bAutoWidth": null, - /** - * Callback functions for when the table has been initialised. - * @type array - * @default [] - */ - "aoInitComplete": [], + /** + * Delay the creation of TR and TD elements until they are actually + * needed by a driven page draw. This can give a significant speed + * increase for Ajax source and Javascript source data, but makes no + * difference at all for DOM and server-side processing tables. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bDeferRender": null, + + /** + * Enable filtering on the table or not. Note that if this is disabled + * then there is no filtering at all on the table, including fnFilter. + * To just remove the filtering input use sDom and remove the 'f' option. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bFilter": null, + + /** + * Used only for compatiblity with DT1 + * @deprecated + */ + "bInfo": true, + + /** + * Used only for compatiblity with DT1 + * @deprecated + */ + "bLengthChange": true, + + /** + * Pagination enabled or not. Note that if this is disabled then length + * changing must also be disabled. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bPaginate": null, + + /** + * Processing indicator enable flag whenever DataTables is enacting a + * user request - typically an Ajax request for server-side processing. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bProcessing": null, + /** + * Server-side processing enabled flag - when enabled DataTables will + * get all data from the server for every draw - there is no filtering, + * sorting or paging done on the client-side. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bServerSide": null, - /** - * Callbacks for modifying the settings to be stored for state saving, prior to - * saving state. - * @type array - * @default [] - */ - "aoStateSaveParams": [], + /** + * Sorting enablement flag. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bSort": null, - /** - * Callbacks for modifying the settings that have been stored for state saving - * prior to using the stored values to restore the state. - * @type array - * @default [] - */ - "aoStateLoadParams": [], + /** + * Multi-column sorting + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bSortMulti": null, - /** - * Callbacks for operating on the settings object once the saved state has been - * loaded - * @type array - * @default [] - */ - "aoStateLoaded": [], + /** + * Apply a class to the columns which are being sorted to provide a + * visual highlight or not. This can slow things down when enabled since + * there is a lot of DOM interaction. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bSortClasses": null, - /** - * Cache the table ID for quick access - * @type string - * @default <i>Empty string</i> - */ - "sTableId": "", + /** + * State saving enablement flag. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bStateSave": null + }, - /** - * The TABLE node for the main table - * @type node - * @default null - */ - "nTable": null, /** - * Permanent ref to the thead element - * @type node - * @default null + * Scrolling settings for a table. */ - "nTHead": null, + "oScroll": { + /** + * When the table is shorter in height than sScrollY, collapse the + * table container down to the height of the table (when true). + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bCollapse": null, - /** - * Permanent ref to the tfoot element - if it exists - * @type node - * @default null - */ - "nTFoot": null, + /** + * Width of the scrollbar for the web-browser's platform. Calculated + * during table initialisation. + */ + "iBarWidth": 0, - /** - * Permanent ref to the tbody element - * @type node - * @default null - */ - "nTBody": null, + /** + * Viewport width for horizontal scrolling. Horizontal scrolling is + * disabled if an empty string. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "sX": null, - /** - * Cache the wrapper node (contains all DataTables controlled elements) - * @type node - * @default null - */ - "nTableWrapper": null, + /** + * Width to expand the table to when using x-scrolling. Typically you + * should not need to use this. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + * @deprecated + */ + "sXInner": null, - /** - * Indicate if when using server-side processing the loading of data - * should be deferred until the second draw. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean - * @default false - */ - "bDeferLoading": false, + /** + * Viewport height for vertical scrolling. Vertical scrolling is disabled + * if an empty string. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "sY": null + }, /** - * Indicate if all required information has been read in - * @type boolean - * @default false + * Language information for the table. */ - "bInitialised": false, + "oLanguage": { + /** + * Information callback function. See + * {@link DataTable.defaults.fnInfoCallback} + */ + "fnInfoCallback": null + }, /** - * Information about open rows. Each object in the array has the parameters - * 'nTr' and 'nParent' - * @type array - * @default [] + * Browser support parameters */ - "aoOpenRows": [], + "oBrowser": { + /** + * Determine if the vertical scrollbar is on the right or left of the + * scrolling container - needed for rtl language layout, although not + * all browsers move the scrollbar (Safari). + */ + "bScrollbarLeft": false, - /** - * Dictate the positioning of DataTables' control elements - see - * {@link DataTable.model.oInit.sDom}. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type string - * @default null - */ - "sDom": null, + /** + * Browser scrollbar width + */ + "barWidth": 0 + }, - /** - * Search delay (in mS) - * @type integer - * @default null - */ - "searchDelay": null, - /** - * Which type of pagination should be used. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type string - * @default two_button - */ - "sPaginationType": "two_button", + "ajax": null, - /** - * The state duration (for `stateSave`) in seconds. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type int - * @default 0 - */ - "iStateDuration": 0, /** - * Array of callback functions for state saving. Each array element is an - * object with the following parameters: + * Array referencing the nodes which are used for the features. The + * parameters of this object match what is allowed by sDom - i.e. * <ul> - * <li>function:fn - function to call. Takes two parameters, oSettings - * and the JSON string to save that has been thus far created. Returns - * a JSON string to be inserted into a json object - * (i.e. '"param": [ 0, 1, 2]')</li> - * <li>string:sName - name of callback</li> + * <li>'l' - Length changing</li> + * <li>'f' - Filtering input</li> + * <li>'t' - The table!</li> + * <li>'i' - Information</li> + * <li>'p' - Pagination</li> + * <li>'r' - pRocessing</li> * </ul> - * @type array - * @default [] */ - "aoStateSave": [], + "aanFeatures": [], /** - * Array of callback functions for state loading. Each array element is an - * object with the following parameters: - * <ul> - * <li>function:fn - function to call. Takes two parameters, oSettings - * and the object stored. May return false to cancel state loading</li> - * <li>string:sName - name of callback</li> - * </ul> - * @type array - * @default [] + * Store data information - see {@link DataTable.models.oRow} for detailed + * information. */ - "aoStateLoad": [], + "aoData": [], /** - * State that was saved. Useful for back reference - * @type object - * @default null + * Array of indexes which are in the current display (after filtering etc) */ - "oSavedState": null, + "aiDisplay": [], /** - * State that was loaded. Useful for back reference - * @type object - * @default null + * Array of indexes for display - no filtering */ - "oLoadedState": null, + "aiDisplayMaster": [], /** - * Source url for AJAX data for the table. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type string - * @default null + * Map of row ids to data indexes */ - "sAjaxSource": null, + "aIds": {}, /** - * Property from a given object from which to read the table data from. This - * can be an empty string (when not server-side processing), in which case - * it is assumed an an array is given directly. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type string + * Store information about each column that is in use */ - "sAjaxDataProp": null, + "aoColumns": [], /** - * The last jQuery XHR object that was used for server-side data gathering. - * This can be used for working with the XHR information in one of the - * callbacks - * @type object - * @default null + * Store information about the table's header */ - "jqXHR": null, + "aoHeader": [], /** - * JSON returned from the server in the last Ajax request - * @type object - * @default undefined + * Store information about the table's footer */ - "json": undefined, + "aoFooter": [], /** - * Data submitted as part of the last Ajax request - * @type object - * @default undefined + * Store the applied global search information in case we want to force a + * research or compare the old search to a new one. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. */ - "oAjaxData": undefined, + "oPreviousSearch": {}, /** - * Function to get the server-side data. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type function + * Store for named searches */ - "fnServerData": null, + searchFixed: {}, /** - * Functions which are called prior to sending an Ajax request so extra - * parameters can easily be sent to the server - * @type array - * @default [] + * Store the applied search for each column - see + * {@link DataTable.models.oSearch} for the format that is used for the + * filtering information for each column. */ - "aoServerParams": [], + "aoPreSearchCols": [], /** - * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if - * required). + * Sorting that is applied to the table. Note that the inner arrays are + * used in the following manner: + * <ul> + * <li>Index 0 - column number</li> + * <li>Index 1 - current sorting direction</li> + * </ul> * Note that this parameter will be set by the initialisation routine. To * set a default use {@link DataTable.defaults}. - * @type string */ - "sServerMethod": null, + "aaSorting": null, /** - * Format numbers for display. + * Sorting that is always applied to the table (i.e. prefixed in front of + * aaSorting). * Note that this parameter will be set by the initialisation routine. To * set a default use {@link DataTable.defaults}. - * @type function */ - "fnFormatNumber": null, + "aaSortingFixed": [], /** - * List of options that can be used for the user selectable length menu. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type array - * @default [] + * If restoring a table - we should restore its width */ - "aLengthMenu": null, + "sDestroyWidth": 0, /** - * Counter for the draws that the table does. Also used as a tracker for - * server-side processing - * @type int - * @default 0 + * Callback functions array for every time a row is inserted (i.e. on a draw). */ - "iDraw": 0, + "aoRowCallback": [], /** - * Indicate if a redraw is being done - useful for Ajax - * @type boolean - * @default false + * Callback functions for the header on each draw. */ - "bDrawing": false, + "aoHeaderCallback": [], /** - * Draw index (iDraw) of the last error when parsing the returned data - * @type int - * @default -1 + * Callback function for the footer on each draw. */ - "iDrawError": -1, + "aoFooterCallback": [], /** - * Paging display length - * @type int - * @default 10 + * Array of callback functions for draw callback functions */ - "_iDisplayLength": 10, + "aoDrawCallback": [], /** - * Paging start point - aiDisplay index - * @type int - * @default 0 + * Array of callback functions for row created function */ - "_iDisplayStart": 0, + "aoRowCreatedCallback": [], /** - * Server-side processing - number of records in the result set - * (i.e. before filtering), Use fnRecordsTotal rather than - * this property to get the value of the number of records, regardless of - * the server-side processing setting. - * @type int - * @default 0 - * @private + * Callback functions for just before the table is redrawn. A return of + * false will be used to cancel the draw. */ - "_iRecordsTotal": 0, + "aoPreDrawCallback": [], /** - * Server-side processing - number of records in the current display set - * (i.e. after filtering). Use fnRecordsDisplay rather than - * this property to get the value of the number of records, regardless of - * the server-side processing setting. - * @type boolean - * @default 0 - * @private + * Callback functions for when the table has been initialised. */ - "_iRecordsDisplay": 0, + "aoInitComplete": [], - /** - * The classes to use for the table - * @type object - * @default {} - */ - "oClasses": {}, /** - * Flag attached to the settings object so you can check in the draw - * callback if filtering has been done in the draw. Deprecated in favour of - * events. - * @type boolean - * @default false - * @deprecated + * Callbacks for modifying the settings to be stored for state saving, prior to + * saving state. */ - "bFiltered": false, + "aoStateSaveParams": [], /** - * Flag attached to the settings object so you can check in the draw - * callback if sorting has been done in the draw. Deprecated in favour of - * events. - * @type boolean - * @default false - * @deprecated + * Callbacks for modifying the settings that have been stored for state saving + * prior to using the stored values to restore the state. */ - "bSorted": false, + "aoStateLoadParams": [], /** - * Indicate that if multiple rows are in the header and there is more than - * one unique cell per column, if the top one (true) or bottom one (false) - * should be used for sorting / title by DataTables. - * Note that this parameter will be set by the initialisation routine. To - * set a default use {@link DataTable.defaults}. - * @type boolean + * Callbacks for operating on the settings object once the saved state has been + * loaded */ - "bSortCellsTop": null, + "aoStateLoaded": [], /** - * Initialisation object that is used for the table - * @type object - * @default null + * Cache the table ID for quick access */ - "oInit": null, + "sTableId": "", /** - * Destroy callback functions - for plug-ins to attach themselves to the - * destroy so they can clean up markup and events. - * @type array - * @default [] + * The TABLE node for the main table */ - "aoDestroyCallback": [], - + "nTable": null, /** - * Get the number of records in the current record set, before filtering - * @type function + * Permanent ref to the thead element */ - "fnRecordsTotal": function () - { - return _fnDataSource( this ) == 'ssp' ? - this._iRecordsTotal * 1 : - this.aiDisplayMaster.length; - }, + "nTHead": null, /** - * Get the number of records in the current record set, after filtering - * @type function + * Permanent ref to the tfoot element - if it exists */ - "fnRecordsDisplay": function () - { - return _fnDataSource( this ) == 'ssp' ? - this._iRecordsDisplay * 1 : - this.aiDisplay.length; - }, + "nTFoot": null, /** - * Get the display end point - aiDisplay index - * @type function + * Permanent ref to the tbody element */ - "fnDisplayEnd": function () - { - var - len = this._iDisplayLength, - start = this._iDisplayStart, - calc = start + len, - records = this.aiDisplay.length, - features = this.oFeatures, - paginate = features.bPaginate; - - if ( features.bServerSide ) { - return paginate === false || len === -1 ? - start + records : - Math.min( start+len, this._iRecordsDisplay ); - } - else { - return ! paginate || calc>records || len===-1 ? - records : - calc; - } - }, + "nTBody": null, /** - * The DataTables object for this table - * @type object - * @default null + * Cache the wrapper node (contains all DataTables controlled elements) */ - "oInstance": null, + "nTableWrapper": null, /** - * Unique identifier for each instance of the DataTables object. If there - * is an ID on the table node, then it takes that value, otherwise an - * incrementing internal counter is used. - * @type string - * @default null + * Indicate if all required information has been read in */ - "sInstance": null, + "bInitialised": false, /** - * tabindex attribute value that is added to DataTables control elements, allowing - * keyboard navigation of the table and its controls. + * Information about open rows. Each object in the array has the parameters + * 'nTr' and 'nParent' */ - "iTabIndex": 0, + "aoOpenRows": [], /** - * DIV container for the footer scrolling table if scrolling + * Dictate the positioning of DataTables' control elements - see + * {@link DataTable.model.oInit.sDom}. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. */ - "nScrollHead": null, + "sDom": null, /** - * DIV container for the footer scrolling table if scrolling + * Search delay (in mS) */ - "nScrollFoot": null, + "searchDelay": null, /** - * Last applied sort - * @type array - * @default [] + * Which type of pagination should be used. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. */ - "aLastSort": [], + "sPaginationType": "two_button", /** - * Stored plug-in instances - * @type object - * @default {} + * Number of paging controls on the page. Only used for backwards compatibility */ - "oPlugins": {}, + pagingControls: 0, /** - * Function used to get a row's id from the row's data - * @type function - * @default null + * The state duration (for `stateSave`) in seconds. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. */ - "rowIdFn": null, + "iStateDuration": 0, /** - * Data location where to store a row's id - * @type string - * @default null + * Array of callback functions for state saving. Each array element is an + * object with the following parameters: + * <ul> + * <li>function:fn - function to call. Takes two parameters, oSettings + * and the JSON string to save that has been thus far created. Returns + * a JSON string to be inserted into a json object + * (i.e. '"param": [ 0, 1, 2]')</li> + * <li>string:sName - name of callback</li> + * </ul> */ - "rowId": null - }; - - /** - * Extension object for DataTables that is used to provide all extension - * options. - * - * Note that the `DataTable.ext` object is available through - * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is - * also aliased to `jQuery.fn.dataTableExt` for historic reasons. - * @namespace - * @extends DataTable.models.ext - */ - + "aoStateSave": [], - /** - * DataTables extensions - * - * This namespace acts as a collection area for plug-ins that can be used to - * extend DataTables capabilities. Indeed many of the build in methods - * use this method to provide their own capabilities (sorting methods for - * example). - * - * Note that this namespace is aliased to `jQuery.fn.dataTableExt` for legacy - * reasons - * - * @namespace - */ - DataTable.ext = _ext = { /** - * Buttons. For use with the Buttons extension for DataTables. This is - * defined here so other extensions can define buttons regardless of load - * order. It is _not_ used by DataTables core. - * - * @type object - * @default {} + * Array of callback functions for state loading. Each array element is an + * object with the following parameters: + * <ul> + * <li>function:fn - function to call. Takes two parameters, oSettings + * and the object stored. May return false to cancel state loading</li> + * <li>string:sName - name of callback</li> + * </ul> */ - buttons: {}, + "aoStateLoad": [], + /** + * State that was saved. Useful for back reference + */ + "oSavedState": null, /** - * Element class names - * - * @type object - * @default {} + * State that was loaded. Useful for back reference */ - classes: {}, + "oLoadedState": null, + /** + * Note if draw should be blocked while getting data + */ + "bAjaxDataGet": true, /** - * DataTables build type (expanded by the download builder) - * - * @type string + * The last jQuery XHR object that was used for server-side data gathering. + * This can be used for working with the XHR information in one of the + * callbacks */ - build:"bs5/dt-1.13.6", + "jqXHR": null, + /** + * JSON returned from the server in the last Ajax request + */ + "json": undefined, /** - * Error reporting. - * - * How should DataTables report an error. Can take the value 'alert', - * 'throw', 'none' or a function. - * - * @type string|function - * @default alert + * Data submitted as part of the last Ajax request */ - errMode: "alert", + "oAjaxData": undefined, + /** + * Send the XHR HTTP method - GET or POST (could be PUT or DELETE if + * required). + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "sServerMethod": null, /** - * Feature plug-ins. - * - * This is an array of objects which describe the feature plug-ins that are - * available to DataTables. These feature plug-ins are then available for - * use through the `dom` initialisation option. - * - * Each feature plug-in is described by an object which must have the - * following properties: - * - * * `fnInit` - function that is used to initialise the plug-in, - * * `cFeature` - a character so the feature can be enabled by the `dom` - * instillation option. This is case sensitive. - * - * The `fnInit` function has the following input parameters: - * - * 1. `{object}` DataTables settings object: see - * {@link DataTable.models.oSettings} - * - * And the following return is expected: - * - * * {node|null} The element which contains your feature. Note that the - * return may also be void if your plug-in does not require to inject any - * DOM elements into DataTables control (`dom`) - for example this might - * be useful when developing a plug-in which allows table control via - * keyboard entry - * - * @type array - * - * @example - * $.fn.dataTable.ext.features.push( { - * "fnInit": function( oSettings ) { - * return new TableTools( { "oDTSettings": oSettings } ); - * }, - * "cFeature": "T" - * } ); + * Format numbers for display. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. */ - feature: [], + "fnFormatNumber": null, + /** + * List of options that can be used for the user selectable length menu. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "aLengthMenu": null, /** - * Row searching. - * - * This method of searching is complimentary to the default type based - * searching, and a lot more comprehensive as it allows you complete control - * over the searching logic. Each element in this array is a function - * (parameters described below) that is called for every row in the table, - * and your logic decides if it should be included in the searching data set - * or not. - * - * Searching functions have the following input parameters: - * - * 1. `{object}` DataTables settings object: see - * {@link DataTable.models.oSettings} - * 2. `{array|object}` Data for the row to be processed (same as the - * original format that was passed in as the data source, or an array - * from a DOM data source - * 3. `{int}` Row index ({@link DataTable.models.oSettings.aoData}), which - * can be useful to retrieve the `TR` element if you need DOM interaction. - * - * And the following return is expected: - * - * * {boolean} Include the row in the searched result set (true) or not - * (false) - * - * Note that as with the main search ability in DataTables, technically this - * is "filtering", since it is subtractive. However, for consistency in - * naming we call it searching here. - * - * @type array - * @default [] - * - * @example - * // The following example shows custom search being applied to the - * // fourth column (i.e. the data[3] index) based on two input values - * // from the end-user, matching the data in a certain range. - * $.fn.dataTable.ext.search.push( - * function( settings, data, dataIndex ) { - * var min = document.getElementById('min').value * 1; - * var max = document.getElementById('max').value * 1; - * var version = data[3] == "-" ? 0 : data[3]*1; - * - * if ( min == "" && max == "" ) { - * return true; - * } - * else if ( min == "" && version < max ) { - * return true; - * } - * else if ( min < version && "" == max ) { - * return true; - * } - * else if ( min < version && version < max ) { - * return true; - * } - * return false; - * } - * ); + * Counter for the draws that the table does. Also used as a tracker for + * server-side processing */ - search: [], + "iDraw": 0, + /** + * Indicate if a redraw is being done - useful for Ajax + */ + "bDrawing": false, /** - * Selector extensions - * - * The `selector` option can be used to extend the options available for the - * selector modifier options (`selector-modifier` object data type) that - * each of the three built in selector types offer (row, column and cell + - * their plural counterparts). For example the Select extension uses this - * mechanism to provide an option to select only rows, columns and cells - * that have been marked as selected by the end user (`{selected: true}`), - * which can be used in conjunction with the existing built in selector - * options. - * - * Each property is an array to which functions can be pushed. The functions - * take three attributes: - * - * * Settings object for the host table - * * Options object (`selector-modifier` object type) - * * Array of selected item indexes - * - * The return is an array of the resulting item indexes after the custom - * selector has been applied. - * - * @type object + * Draw index (iDraw) of the last error when parsing the returned data */ - selector: { - cell: [], - column: [], - row: [] - }, + "iDrawError": -1, + /** + * Paging display length + */ + "_iDisplayLength": 10, /** - * Internal functions, exposed for used in plug-ins. - * - * Please note that you should not need to use the internal methods for - * anything other than a plug-in (and even then, try to avoid if possible). - * The internal function may change between releases. - * - * @type object - * @default {} + * Paging start point - aiDisplay index */ - internal: {}, + "_iDisplayStart": 0, + /** + * Server-side processing - number of records in the result set + * (i.e. before filtering), Use fnRecordsTotal rather than + * this property to get the value of the number of records, regardless of + * the server-side processing setting. + */ + "_iRecordsTotal": 0, /** - * Legacy configuration options. Enable and disable legacy options that - * are available in DataTables. - * - * @type object + * Server-side processing - number of records in the current display set + * (i.e. after filtering). Use fnRecordsDisplay rather than + * this property to get the value of the number of records, regardless of + * the server-side processing setting. */ - legacy: { - /** - * Enable / disable DataTables 1.9 compatible server-side processing - * requests - * - * @type boolean - * @default null - */ - ajax: null - }, + "_iRecordsDisplay": 0, + /** + * The classes to use for the table + */ + "oClasses": {}, /** - * Pagination plug-in methods. - * - * Each entry in this object is a function and defines which buttons should - * be shown by the pagination rendering method that is used for the table: - * {@link DataTable.ext.renderer.pageButton}. The renderer addresses how the - * buttons are displayed in the document, while the functions here tell it - * what buttons to display. This is done by returning an array of button - * descriptions (what each button will do). - * - * Pagination types (the four built in options and any additional plug-in - * options defined here) can be used through the `paginationType` - * initialisation parameter. - * - * The functions defined take two parameters: - * - * 1. `{int} page` The current page index - * 2. `{int} pages` The number of pages in the table - * - * Each function is expected to return an array where each element of the - * array can be one of: - * - * * `first` - Jump to first page when activated - * * `last` - Jump to last page when activated - * * `previous` - Show previous page when activated - * * `next` - Show next page when activated - * * `{int}` - Show page of the index given - * * `{array}` - A nested array containing the above elements to add a - * containing 'DIV' element (might be useful for styling). - * - * Note that DataTables v1.9- used this object slightly differently whereby - * an object with two functions would be defined for each plug-in. That - * ability is still supported by DataTables 1.10+ to provide backwards - * compatibility, but this option of use is now decremented and no longer - * documented in DataTables 1.10+. - * - * @type object - * @default {} - * - * @example - * // Show previous, next and current page buttons only - * $.fn.dataTableExt.oPagination.current = function ( page, pages ) { - * return [ 'previous', page, 'next' ]; - * }; + * Flag attached to the settings object so you can check in the draw + * callback if filtering has been done in the draw. Deprecated in favour of + * events. + * @deprecated + */ + "bFiltered": false, + + /** + * Flag attached to the settings object so you can check in the draw + * callback if sorting has been done in the draw. Deprecated in favour of + * events. + * @deprecated */ - pager: {}, - + "bSorted": false, - renderer: { - pageButton: {}, - header: {} - }, + /** + * Indicate that if multiple rows are in the header and there is more than + * one unique cell per column, if the top one (true) or bottom one (false) + * should be used for sorting / title by DataTables. + * Note that this parameter will be set by the initialisation routine. To + * set a default use {@link DataTable.defaults}. + */ + "bSortCellsTop": null, + /** + * Initialisation object that is used for the table + */ + "oInit": null, /** - * Ordering plug-ins - custom data source - * - * The extension options for ordering of data available here is complimentary - * to the default type based ordering that DataTables typically uses. It - * allows much greater control over the the data that is being used to - * order a column, but is necessarily therefore more complex. - * - * This type of ordering is useful if you want to do ordering based on data - * live from the DOM (for example the contents of an 'input' element) rather - * than just the static string that DataTables knows of. - * - * The way these plug-ins work is that you create an array of the values you - * wish to be ordering for the column in question and then return that - * array. The data in the array much be in the index order of the rows in - * the table (not the currently ordering order!). Which order data gathering - * function is run here depends on the `dt-init columns.orderDataType` - * parameter that is used for the column (if any). - * - * The functions defined take two parameters: - * - * 1. `{object}` DataTables settings object: see - * {@link DataTable.models.oSettings} - * 2. `{int}` Target column index - * - * Each function is expected to return an array: - * - * * `{array}` Data for the column to be ordering upon - * - * @type array - * - * @example - * // Ordering using `input` node values - * $.fn.dataTable.ext.order['dom-text'] = function ( settings, col ) - * { - * return this.api().column( col, {order:'index'} ).nodes().map( function ( td, i ) { - * return $('input', td).val(); - * } ); - * } + * Destroy callback functions - for plug-ins to attach themselves to the + * destroy so they can clean up markup and events. */ - order: {}, + "aoDestroyCallback": [], /** - * Type based plug-ins. - * - * Each column in DataTables has a type assigned to it, either by automatic - * detection or by direct assignment using the `type` option for the column. - * The type of a column will effect how it is ordering and search (plug-ins - * can also make use of the column type if required). - * - * @namespace + * Get the number of records in the current record set, before filtering */ - type: { - /** - * Type detection functions. - * - * The functions defined in this object are used to automatically detect - * a column's type, making initialisation of DataTables super easy, even - * when complex data is in the table. - * - * The functions defined take two parameters: - * - * 1. `{*}` Data from the column cell to be analysed - * 2. `{settings}` DataTables settings object. This can be used to - * perform context specific type detection - for example detection - * based on language settings such as using a comma for a decimal - * place. Generally speaking the options from the settings will not - * be required - * - * Each function is expected to return: - * - * * `{string|null}` Data type detected, or null if unknown (and thus - * pass it on to the other type detection functions. - * - * @type array - * - * @example - * // Currency type detection plug-in: - * $.fn.dataTable.ext.type.detect.push( - * function ( data, settings ) { - * // Check the numeric part - * if ( ! data.substring(1).match(/[0-9]/) ) { - * return null; - * } - * - * // Check prefixed by currency - * if ( data.charAt(0) == '$' || data.charAt(0) == '&pound;' ) { - * return 'currency'; - * } - * return null; - * } - * ); - */ - detect: [], - + "fnRecordsTotal": function () + { + return _fnDataSource( this ) == 'ssp' ? + this._iRecordsTotal * 1 : + this.aiDisplayMaster.length; + }, - /** - * Type based search formatting. - * - * The type based searching functions can be used to pre-format the - * data to be search on. For example, it can be used to strip HTML - * tags or to de-format telephone numbers for numeric only searching. - * - * Note that is a search is not defined for a column of a given type, - * no search formatting will be performed. - * - * Pre-processing of searching data plug-ins - When you assign the sType - * for a column (or have it automatically detected for you by DataTables - * or a type detection plug-in), you will typically be using this for - * custom sorting, but it can also be used to provide custom searching - * by allowing you to pre-processing the data and returning the data in - * the format that should be searched upon. This is done by adding - * functions this object with a parameter name which matches the sType - * for that target column. This is the corollary of <i>afnSortData</i> - * for searching data. - * - * The functions defined take a single parameter: - * - * 1. `{*}` Data from the column cell to be prepared for searching - * - * Each function is expected to return: - * - * * `{string|null}` Formatted string that will be used for the searching. - * - * @type object - * @default {} - * - * @example - * $.fn.dataTable.ext.type.search['title-numeric'] = function ( d ) { - * return d.replace(/\n/g," ").replace( /<.*?>/g, "" ); - * } - */ - search: {}, + /** + * Get the number of records in the current record set, after filtering + */ + "fnRecordsDisplay": function () + { + return _fnDataSource( this ) == 'ssp' ? + this._iRecordsDisplay * 1 : + this.aiDisplay.length; + }, + /** + * Get the display end point - aiDisplay index + */ + "fnDisplayEnd": function () + { + var + len = this._iDisplayLength, + start = this._iDisplayStart, + calc = start + len, + records = this.aiDisplay.length, + features = this.oFeatures, + paginate = features.bPaginate; - /** - * Type based ordering. - * - * The column type tells DataTables what ordering to apply to the table - * when a column is sorted upon. The order for each type that is defined, - * is defined by the functions available in this object. - * - * Each ordering option can be described by three properties added to - * this object: - * - * * `{type}-pre` - Pre-formatting function - * * `{type}-asc` - Ascending order function - * * `{type}-desc` - Descending order function - * - * All three can be used together, only `{type}-pre` or only - * `{type}-asc` and `{type}-desc` together. It is generally recommended - * that only `{type}-pre` is used, as this provides the optimal - * implementation in terms of speed, although the others are provided - * for compatibility with existing Javascript sort functions. - * - * `{type}-pre`: Functions defined take a single parameter: - * - * 1. `{*}` Data from the column cell to be prepared for ordering - * - * And return: - * - * * `{*}` Data to be sorted upon - * - * `{type}-asc` and `{type}-desc`: Functions are typical Javascript sort - * functions, taking two parameters: - * - * 1. `{*}` Data to compare to the second parameter - * 2. `{*}` Data to compare to the first parameter - * - * And returning: - * - * * `{*}` Ordering match: <0 if first parameter should be sorted lower - * than the second parameter, ===0 if the two parameters are equal and - * >0 if the first parameter should be sorted height than the second - * parameter. - * - * @type object - * @default {} - * - * @example - * // Numeric ordering of formatted numbers with a pre-formatter - * $.extend( $.fn.dataTable.ext.type.order, { - * "string-pre": function(x) { - * a = (a === "-" || a === "") ? 0 : a.replace( /[^\d\-\.]/g, "" ); - * return parseFloat( a ); - * } - * } ); - * - * @example - * // Case-sensitive string ordering, with no pre-formatting method - * $.extend( $.fn.dataTable.ext.order, { - * "string-case-asc": function(x,y) { - * return ((x < y) ? -1 : ((x > y) ? 1 : 0)); - * }, - * "string-case-desc": function(x,y) { - * return ((x < y) ? 1 : ((x > y) ? -1 : 0)); - * } - * } ); - */ - order: {} + if ( features.bServerSide ) { + return paginate === false || len === -1 ? + start + records : + Math.min( start+len, this._iRecordsDisplay ); + } + else { + return ! paginate || calc>records || len===-1 ? + records : + calc; + } }, /** - * Unique DataTables instance counter - * - * @type int - * @private + * The DataTables object for this table + */ + "oInstance": null, + + /** + * Unique identifier for each instance of the DataTables object. If there + * is an ID on the table node, then it takes that value, otherwise an + * incrementing internal counter is used. */ - _unique: 0, - - - // - // Depreciated - // The following properties are retained for backwards compatibility only. - // The should not be used in new projects and will be removed in a future - // version - // + "sInstance": null, /** - * Version check function. - * @type function - * @depreciated Since 1.10 + * tabindex attribute value that is added to DataTables control elements, allowing + * keyboard navigation of the table and its controls. */ - fnVersionCheck: DataTable.fnVersionCheck, + "iTabIndex": 0, + /** + * DIV container for the footer scrolling table if scrolling + */ + "nScrollHead": null, /** - * Index for what 'this' index API functions should use - * @type int - * @deprecated Since v1.10 + * DIV container for the footer scrolling table if scrolling */ - iApiIndex: 0, + "nScrollFoot": null, + /** + * Last applied sort + */ + "aLastSort": [], /** - * jQuery UI class container - * @type object - * @deprecated Since v1.10 + * Stored plug-in instances */ - oJUIClasses: {}, + "oPlugins": {}, + /** + * Function used to get a row's id from the row's data + */ + "rowIdFn": null, /** - * Software version - * @type string - * @deprecated Since v1.10 + * Data location where to store a row's id */ - sVersion: DataTable.version + "rowId": null, + + caption: '', + + captionNode: null, + + colgroup: null }; + /** + * Extension object for DataTables that is used to provide all extension + * options. + * + * Note that the `DataTable.ext` object is available through + * `jQuery.fn.dataTable.ext` where it may be accessed and manipulated. It is + * also aliased to `jQuery.fn.dataTableExt` for historic reasons. + * @namespace + * @extends DataTable.models.ext + */ + - // - // Backwards compatibility. Alias to pre 1.10 Hungarian notation counter parts - // - $.extend( _ext, { - afnFiltering: _ext.search, - aTypes: _ext.type.detect, - ofnSearch: _ext.type.search, - oSort: _ext.type.order, - afnSortData: _ext.order, - aoFeatures: _ext.feature, - oApi: _ext.internal, - oStdClasses: _ext.classes, - oPagination: _ext.pager + var extPagination = DataTable.ext.pager; + + // Paging buttons configuration + $.extend( extPagination, { + simple: function () { + return [ 'previous', 'next' ]; + }, + + full: function () { + return [ 'first', 'previous', 'next', 'last' ]; + }, + + numbers: function () { + return [ 'numbers' ]; + }, + + simple_numbers: function () { + return [ 'previous', 'numbers', 'next' ]; + }, + + full_numbers: function () { + return [ 'first', 'previous', 'numbers', 'next', 'last' ]; + }, + + first_last: function () { + return ['first', 'last']; + }, + + first_last_numbers: function () { + return ['first', 'numbers', 'last']; + }, + + // For testing and plug-ins to use + _numbers: _pagingNumbers, + + // Number of number buttons - legacy, use `numbers` option for paging feature + numbers_length: 7 } ); - $.extend( DataTable.ext.classes, { - "sTable": "dataTable", - "sNoFooter": "no-footer", - - /* Paging buttons */ - "sPageButton": "paginate_button", - "sPageButtonActive": "current", - "sPageButtonDisabled": "disabled", - - /* Striping classes */ - "sStripeOdd": "odd", - "sStripeEven": "even", - - /* Empty row */ - "sRowEmpty": "dataTables_empty", - - /* Features */ - "sWrapper": "dataTables_wrapper", - "sFilter": "dataTables_filter", - "sInfo": "dataTables_info", - "sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */ - "sLength": "dataTables_length", - "sProcessing": "dataTables_processing", - - /* Sorting */ - "sSortAsc": "sorting_asc", - "sSortDesc": "sorting_desc", - "sSortable": "sorting", /* Sortable in both directions */ - "sSortableAsc": "sorting_desc_disabled", - "sSortableDesc": "sorting_asc_disabled", - "sSortableNone": "sorting_disabled", - "sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */ - - /* Filtering */ - "sFilterInput": "", - - /* Page length */ - "sLengthSelect": "", - - /* Scrolling */ - "sScrollWrapper": "dataTables_scroll", - "sScrollHead": "dataTables_scrollHead", - "sScrollHeadInner": "dataTables_scrollHeadInner", - "sScrollBody": "dataTables_scrollBody", - "sScrollFoot": "dataTables_scrollFoot", - "sScrollFootInner": "dataTables_scrollFootInner", - - /* Misc */ - "sHeaderTH": "", - "sFooterTH": "", - - // Deprecated - "sSortJUIAsc": "", - "sSortJUIDesc": "", - "sSortJUI": "", - "sSortJUIAscAllowed": "", - "sSortJUIDescAllowed": "", - "sSortJUIWrapper": "", - "sSortIcon": "", - "sJUIHeader": "", - "sJUIFooter": "" + $.extend( true, DataTable.ext.renderer, { + pagingButton: { + _: function (settings, buttonType, content, active, disabled) { + var classes = settings.oClasses.paging; + var btnClasses = [classes.button]; + var btn; + + if (active) { + btnClasses.push(classes.active); + } + + if (disabled) { + btnClasses.push(classes.disabled) + } + + if (buttonType === 'ellipsis') { + btn = $('<span class="ellipsis"></span>').html(content)[0]; + } + else { + btn = $('<button>', { + class: btnClasses.join(' '), + role: 'link', + type: 'button' + }).html(content); + } + + return { + display: btn, + clicker: btn + } + } + }, + + pagingContainer: { + _: function (settings, buttons) { + // No wrapping element - just append directly to the host + return buttons; + } + } } ); + // Common function to remove new lines, strip HTML and diacritic control + var _filterString = function (stripHtml, normalize) { + return function (str) { + if (_empty(str) || typeof str !== 'string') { + return str; + } + + str = str.replace( _re_new_lines, " " ); + + if (stripHtml) { + str = _stripHtml(str); + } + + if (normalize) { + str = _normalize(str, false); + } + + return str; + }; + } + + /* + * Public helper functions. These aren't used internally by DataTables, or + * called by any of the options passed into DataTables, but they can be used + * externally by developers working with DataTables. They are helper functions + * to make working with DataTables a little bit easier. + */ + + function __mldFnName(name) { + return name.replace(/[\W]/g, '_') + } + + // Common logic for moment, luxon or a date action + function __mld( dt, momentFn, luxonFn, dateFn, arg1 ) { + if (window.moment) { + return dt[momentFn]( arg1 ); + } + else if (window.luxon) { + return dt[luxonFn]( arg1 ); + } + + return dateFn ? dt[dateFn]( arg1 ) : dt; + } + + + var __mlWarning = false; + function __mldObj (d, format, locale) { + var dt; + + if (window.moment) { + dt = window.moment.utc( d, format, locale, true ); + + if (! dt.isValid()) { + return null; + } + } + else if (window.luxon) { + dt = format && typeof d === 'string' + ? window.luxon.DateTime.fromFormat( d, format ) + : window.luxon.DateTime.fromISO( d ); + + if (! dt.isValid) { + return null; + } + + dt.setLocale(locale); + } + else if (! format) { + // No format given, must be ISO + dt = new Date(d); + } + else { + if (! __mlWarning) { + alert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17'); + } + + __mlWarning = true; + } + + return dt; + } + + // Wrapper for date, datetime and time which all operate the same way with the exception of + // the output string for auto locale support + function __mlHelper (localeString) { + return function ( from, to, locale, def ) { + // Luxon and Moment support + // Argument shifting + if ( arguments.length === 0 ) { + locale = 'en'; + to = null; // means toLocaleString + from = null; // means iso8601 + } + else if ( arguments.length === 1 ) { + locale = 'en'; + to = from; + from = null; + } + else if ( arguments.length === 2 ) { + locale = to; + to = from; + from = null; + } + + var typeName = 'datetime' + (to ? '-' + __mldFnName(to) : ''); + + // Add type detection and sorting specific to this date format - we need to be able to identify + // date type columns as such, rather than as numbers in extensions. Hence the need for this. + if (! DataTable.ext.type.order[typeName]) { + DataTable.type(typeName, { + detect: function (d) { + // The renderer will give the value to type detect as the type! + return d === typeName ? typeName : false; + }, + order: { + pre: function (d) { + // The renderer gives us Moment, Luxon or Date obects for the sorting, all of which have a + // `valueOf` which gives milliseconds epoch + return d.valueOf(); + } + }, + className: 'dt-right' + }); + } + + return function ( d, type ) { + // Allow for a default value + if (d === null || d === undefined) { + if (def === '--now') { + // We treat everything as UTC further down, so no changes are + // made, as such need to get the local date / time as if it were + // UTC + var local = new Date(); + d = new Date( Date.UTC( + local.getFullYear(), local.getMonth(), local.getDate(), + local.getHours(), local.getMinutes(), local.getSeconds() + ) ); + } + else { + d = ''; + } + } + + if (type === 'type') { + // Typing uses the type name for fast matching + return typeName; + } + + if (d === '') { + return type !== 'sort' + ? '' + : __mldObj('0000-01-01 00:00:00', null, locale); + } + + // Shortcut. If `from` and `to` are the same, we are using the renderer to + // format for ordering, not display - its already in the display format. + if ( to !== null && from === to && type !== 'sort' && type !== 'type' && ! (d instanceof Date) ) { + return d; + } + + var dt = __mldObj(d, from, locale); - var extPagination = DataTable.ext.pager; + if (dt === null) { + return d; + } - function _numbers ( page, pages ) { - var - numbers = [], - buttons = extPagination.numbers_length, - half = Math.floor( buttons / 2 ), - i = 1; + if (type === 'sort') { + return dt; + } + + var formatted = to === null + ? __mld(dt, 'toDate', 'toJSDate', '')[localeString]() + : __mld(dt, 'format', 'toFormat', 'toISOString', to); - if ( pages <= buttons ) { - numbers = _range( 0, pages ); - } - else if ( page <= half ) { - numbers = _range( 0, buttons-2 ); - numbers.push( 'ellipsis' ); - numbers.push( pages-1 ); + // XSS protection + return type === 'display' ? + _escapeHtml( formatted ) : + formatted; + }; } - else if ( page >= pages - 1 - half ) { - numbers = _range( pages-(buttons-2), pages ); - numbers.splice( 0, 0, 'ellipsis' ); // no unshift in ie6 - numbers.splice( 0, 0, 0 ); + } + + // Based on locale, determine standard number formatting + // Fallback for legacy browsers is US English + var __thousands = ','; + var __decimal = '.'; + + if (window.Intl !== undefined) { + try { + var num = new Intl.NumberFormat().formatToParts(100000.1); + + for (var i=0 ; i<num.length ; i++) { + if (num[i].type === 'group') { + __thousands = num[i].value; + } + else if (num[i].type === 'decimal') { + __decimal = num[i].value; + } + } } - else { - numbers = _range( page-half+2, page+half-1 ); - numbers.push( 'ellipsis' ); - numbers.push( pages-1 ); - numbers.splice( 0, 0, 'ellipsis' ); - numbers.splice( 0, 0, 0 ); + catch (e) { + // noop } - - numbers.DT_el = 'span'; - return numbers; } + // Formatted date time detection - use by declaring the formats you are going to use + DataTable.datetime = function ( format, locale ) { + var typeName = 'datetime-detect-' + __mldFnName(format); - $.extend( extPagination, { - simple: function ( page, pages ) { - return [ 'previous', 'next' ]; - }, + if (! locale) { + locale = 'en'; + } - full: function ( page, pages ) { - return [ 'first', 'previous', 'next', 'last' ]; - }, + if (! DataTable.ext.type.order[typeName]) { + DataTable.type(typeName, { + detect: function (d) { + var dt = __mldObj(d, format, locale); + return d === '' || dt ? typeName : false; + }, + order: { + pre: function (d) { + return __mldObj(d, format, locale) || 0; + } + }, + className: 'dt-right' + }); + } + } - numbers: function ( page, pages ) { - return [ _numbers(page, pages) ]; - }, + /** + * Helpers for `columns.render`. + * + * The options defined here can be used with the `columns.render` initialisation + * option to provide a display renderer. The following functions are defined: + * + * * `moment` - Uses the MomentJS library to convert from a given format into another. + * This renderer has three overloads: + * * 1 parameter: + * * `string` - Format to convert to (assumes input is ISO8601 and locale is `en`) + * * 2 parameters: + * * `string` - Format to convert from + * * `string` - Format to convert to. Assumes `en` locale + * * 3 parameters: + * * `string` - Format to convert from + * * `string` - Format to convert to + * * `string` - Locale + * * `number` - Will format numeric data (defined by `columns.data`) for + * display, retaining the original unformatted data for sorting and filtering. + * It takes 5 parameters: + * * `string` - Thousands grouping separator + * * `string` - Decimal point indicator + * * `integer` - Number of decimal points to show + * * `string` (optional) - Prefix. + * * `string` (optional) - Postfix (/suffix). + * * `text` - Escape HTML to help prevent XSS attacks. It has no optional + * parameters. + * + * @example + * // Column definition using the number renderer + * { + * data: "salary", + * render: $.fn.dataTable.render.number( '\'', '.', 0, '$' ) + * } + * + * @namespace + */ + DataTable.render = { + date: __mlHelper('toLocaleDateString'), + datetime: __mlHelper('toLocaleString'), + time: __mlHelper('toLocaleTimeString'), + number: function ( thousands, decimal, precision, prefix, postfix ) { + // Auto locale detection + if (thousands === null || thousands === undefined) { + thousands = __thousands; + } - simple_numbers: function ( page, pages ) { - return [ 'previous', _numbers(page, pages), 'next' ]; - }, + if (decimal === null || decimal === undefined) { + decimal = __decimal; + } - full_numbers: function ( page, pages ) { - return [ 'first', 'previous', _numbers(page, pages), 'next', 'last' ]; - }, - - first_last_numbers: function (page, pages) { - return ['first', _numbers(page, pages), 'last']; - }, + return { + display: function ( d ) { + if ( typeof d !== 'number' && typeof d !== 'string' ) { + return d; + } - // For testing and plug-ins to use - _numbers: _numbers, + if (d === '' || d === null) { + return d; + } - // Number of number buttons (including ellipsis) to show. _Must be odd!_ - numbers_length: 7 - } ); + var negative = d < 0 ? '-' : ''; + var flo = parseFloat( d ); + var abs = Math.abs(flo); + // Scientific notation for large and small numbers + if (abs >= 100000000000 || (abs < 0.0001 && abs !== 0) ) { + var exp = flo.toExponential(precision).split(/e\+?/); + return exp[0] + ' x 10<sup>' + exp[1] + '</sup>'; + } - $.extend( true, DataTable.ext.renderer, { - pageButton: { - _: function ( settings, host, idx, buttons, page, pages ) { - var classes = settings.oClasses; - var lang = settings.oLanguage.oPaginate; - var aria = settings.oLanguage.oAria.paginate || {}; - var btnDisplay, btnClass; - - var attach = function( container, buttons ) { - var i, ien, node, button; - var disabledClass = classes.sPageButtonDisabled; - var clickHandler = function ( e ) { - _fnPageChange( settings, e.data.action, true ); - }; + // If NaN then there isn't much formatting that we can do - just + // return immediately, escaping any HTML (this was supposed to + // be a number after all) + if ( isNaN( flo ) ) { + return _escapeHtml( d ); + } - for ( i=0, ien=buttons.length ; i<ien ; i++ ) { - button = buttons[i]; + flo = flo.toFixed( precision ); + d = Math.abs( flo ); - if ( Array.isArray( button ) ) { - var inner = $( '<'+(button.DT_el || 'div')+'/>' ) - .appendTo( container ); - attach( inner, button ); - } - else { - var disabled = false; + var intPart = parseInt( d, 10 ); + var floatPart = precision ? + decimal+(d - intPart).toFixed( precision ).substring( 2 ): + ''; - btnDisplay = null; - btnClass = button; + // If zero, then can't have a negative prefix + if (intPart === 0 && parseFloat(floatPart) === 0) { + negative = ''; + } - switch ( button ) { - case 'ellipsis': - container.append('<span class="ellipsis">&#x2026;</span>'); - break; + return negative + (prefix||'') + + intPart.toString().replace( + /\B(?=(\d{3})+(?!\d))/g, thousands + ) + + floatPart + + (postfix||''); + } + }; + }, - case 'first': - btnDisplay = lang.sFirst; + text: function () { + return { + display: _escapeHtml, + filter: _escapeHtml + }; + } + }; - if ( page === 0 ) { - disabled = true; - } - break; - case 'previous': - btnDisplay = lang.sPrevious; + var _extTypes = DataTable.ext.type; - if ( page === 0 ) { - disabled = true; - } - break; + // Get / set type + DataTable.type = function (name, prop, val) { + if (! prop) { + return { + className: _extTypes.className[name], + detect: _extTypes.detect.find(function (fn) { + return fn.name === name; + }), + order: { + pre: _extTypes.order[name + '-pre'], + asc: _extTypes.order[name + '-asc'], + desc: _extTypes.order[name + '-desc'] + }, + render: _extTypes.render[name], + search: _extTypes.search[name] + }; + } - case 'next': - btnDisplay = lang.sNext; + var setProp = function(prop, propVal) { + _extTypes[prop][name] = propVal; + }; + var setDetect = function (fn) { + // Wrap to allow the function to return `true` rather than + // specifying the type name. + var cb = function (d, s) { + var ret = fn(d, s); + + return ret === true + ? name + : ret; + }; + Object.defineProperty(cb, "name", {value: name}); - if ( pages === 0 || page === pages-1 ) { - disabled = true; - } - break; + var idx = _extTypes.detect.findIndex(function (fn) { + return fn.name === name; + }); - case 'last': - btnDisplay = lang.sLast; + if (idx === -1) { + _extTypes.detect.unshift(cb); + } + else { + _extTypes.detect.splice(idx, 1, cb); + } + }; + var setOrder = function (obj) { + _extTypes.order[name + '-pre'] = obj.pre; // can be undefined + _extTypes.order[name + '-asc'] = obj.asc; // can be undefined + _extTypes.order[name + '-desc'] = obj.desc; // can be undefined + }; - if ( pages === 0 || page === pages-1 ) { - disabled = true; - } - break; + // prop is optional + if (val === undefined) { + val = prop; + prop = null; + } - default: - btnDisplay = settings.fnFormatNumber( button + 1 ); - btnClass = page === button ? - classes.sPageButtonActive : ''; - break; - } + if (prop === 'className') { + setProp('className', val); + } + else if (prop === 'detect') { + setDetect(val); + } + else if (prop === 'order') { + setOrder(val); + } + else if (prop === 'render') { + setProp('render', val); + } + else if (prop === 'search') { + setProp('search', val); + } + else if (! prop) { + if (val.className) { + setProp('className', val.className); + } - if ( btnDisplay !== null ) { - var tag = settings.oInit.pagingTag || 'a'; + if (val.detect !== undefined) { + setDetect(val.detect); + } - if (disabled) { - btnClass += ' ' + disabledClass; - } + if (val.order) { + setOrder(val.order); + } - node = $('<'+tag+'>', { - 'class': classes.sPageButton+' '+btnClass, - 'aria-controls': settings.sTableId, - 'aria-disabled': disabled ? 'true' : null, - 'aria-label': aria[ button ], - 'role': 'link', - 'aria-current': btnClass === classes.sPageButtonActive ? 'page' : null, - 'data-dt-idx': button, - 'tabindex': disabled ? -1 : settings.iTabIndex, - 'id': idx === 0 && typeof button === 'string' ? - settings.sTableId +'_'+ button : - null - } ) - .html( btnDisplay ) - .appendTo( container ); - - _fnBindAction( - node, {action: button}, clickHandler - ); - } - } - } - }; + if (val.render !== undefined) { + setProp('render', val.render); + } - // IE9 throws an 'unknown error' if document.activeElement is used - // inside an iframe or frame. Try / catch the error. Not good for - // accessibility, but neither are frames. - var activeEl; + if (val.search !== undefined) { + setProp('search', val.search); + } + } + } - try { - // Because this approach is destroying and recreating the paging - // elements, focus is lost on the select button which is bad for - // accessibility. So we want to restore focus once the draw has - // completed - activeEl = $(host).find(document.activeElement).data('dt-idx'); - } - catch (e) {} + // Get a list of types + DataTable.types = function () { + return _extTypes.detect.map(function (fn) { + return fn.name; + }); + }; - attach( $(host).empty(), buttons ); + // + // Built in data types + // - if ( activeEl !== undefined ) { - $(host).find( '[data-dt-idx='+activeEl+']' ).trigger('focus'); - } + DataTable.type('string', { + detect: function () { + return 'string'; + }, + order: { + pre: function ( a ) { + // This is a little complex, but faster than always calling toString, + // http://jsperf.com/tostring-v-check + return _empty(a) ? + '' : + typeof a === 'string' ? + a.toLowerCase() : + ! a.toString ? + '' : + a.toString(); } - } - } ); - + }, + search: _filterString(false, true) + }); - // Built in type detection. See model.ext.aTypes for information about - // what is required from this methods. - $.extend( DataTable.ext.type.detect, [ - // Plain numbers - first since V8 detects some plain numbers as dates - // e.g. Date.parse('55') (but not all, e.g. Date.parse('22')...). - function ( d, settings ) - { - var decimal = settings.oLanguage.sDecimal; - return _isNumber( d, decimal ) ? 'num'+decimal : null; + DataTable.type('html', { + detect: function ( d ) { + return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ? + 'html' : null; + }, + order: { + pre: function ( a ) { + return _empty(a) ? + '' : + a.replace ? + _stripHtml(a).trim().toLowerCase() : + a+''; + } }, + search: _filterString(true, true) + }); + - // Dates (only those recognised by the browser's Date.parse) - function ( d, settings ) + DataTable.type('date', { + className: 'dt-type-date', + detect: function ( d ) { // V8 tries _very_ hard to make a string passed into `Date.parse()` // valid, so we need to use a regex to restrict date formats. Use a @@ -14999,65 +12320,80 @@ var parsed = Date.parse(d); return (parsed !== null && !isNaN(parsed)) || _empty(d) ? 'date' : null; }, + order: { + pre: function ( d ) { + var ts = Date.parse( d ); + return isNaN(ts) ? -Infinity : ts; + } + } + }); + - // Formatted numbers - function ( d, settings ) + DataTable.type('html-num-fmt', { + className: 'dt-type-numeric', + detect: function ( d, settings ) { var decimal = settings.oLanguage.sDecimal; - return _isNumber( d, decimal, true ) ? 'num-fmt'+decimal : null; + return _htmlNumeric( d, decimal, true ) ? 'html-num-fmt' : null; + }, + order: { + pre: function ( d, s ) { + var dp = s.oLanguage.sDecimal; + return __numericReplace( d, dp, _re_html, _re_formatted_numeric ); + } }, + search: _filterString(true, true) + }); + - // HTML numeric - function ( d, settings ) + DataTable.type('html-num', { + className: 'dt-type-numeric', + detect: function ( d, settings ) { var decimal = settings.oLanguage.sDecimal; - return _htmlNumeric( d, decimal ) ? 'html-num'+decimal : null; + return _htmlNumeric( d, decimal ) ? 'html-num' : null; + }, + order: { + pre: function ( d, s ) { + var dp = s.oLanguage.sDecimal; + return __numericReplace( d, dp, _re_html ); + } }, + search: _filterString(true, true) + }); + - // HTML numeric, formatted - function ( d, settings ) + DataTable.type('num-fmt', { + className: 'dt-type-numeric', + detect: function ( d, settings ) { var decimal = settings.oLanguage.sDecimal; - return _htmlNumeric( d, decimal, true ) ? 'html-num-fmt'+decimal : null; + return _isNumber( d, decimal, true ) ? 'num-fmt' : null; }, - - // HTML (this is strict checking - there must be html) - function ( d, settings ) - { - return _empty( d ) || (typeof d === 'string' && d.indexOf('<') !== -1) ? - 'html' : null; + order: { + pre: function ( d, s ) { + var dp = s.oLanguage.sDecimal; + return __numericReplace( d, dp, _re_formatted_numeric ); + } } - ] ); - - - - // Filter formatting functions. See model.ext.ofnSearch for information about - // what is required from these methods. - // - // Note that additional search methods are added for the html numbers and - // html formatted numbers by `_addNumericSort()` when we know what the decimal - // place is + }); - $.extend( DataTable.ext.type.search, { - html: function ( data ) { - return _empty(data) ? - data : - typeof data === 'string' ? - data - .replace( _re_new_lines, " " ) - .replace( _re_html, "" ) : - ''; + DataTable.type('num', { + className: 'dt-type-numeric', + detect: function ( d, settings ) + { + var decimal = settings.oLanguage.sDecimal; + return _isNumber( d, decimal ) ? 'num' : null; }, - - string: function ( data ) { - return _empty(data) ? - data : - typeof data === 'string' ? - data.replace( _re_new_lines, " " ) : - data; + order: { + pre: function (d, s) { + var dp = s.oLanguage.sDecimal; + return __numericReplace( d, dp ); + } } - } ); + }); + @@ -15093,609 +12429,733 @@ }; - // Add the numeric 'deformatting' functions for sorting and search. This is done - // in a function to provide an easy ability for the language options to add - // additional methods if a non-period decimal place is used. - function _addNumericSort ( decimalPlace ) { - $.each( - { - // Plain numbers - "num": function ( d ) { - return __numericReplace( d, decimalPlace ); - }, - - // Formatted numbers - "num-fmt": function ( d ) { - return __numericReplace( d, decimalPlace, _re_formatted_numeric ); - }, - - // HTML numeric - "html-num": function ( d ) { - return __numericReplace( d, decimalPlace, _re_html ); - }, - - // HTML numeric, formatted - "html-num-fmt": function ( d ) { - return __numericReplace( d, decimalPlace, _re_html, _re_formatted_numeric ); - } - }, - function ( key, fn ) { - // Add the ordering method - _ext.type.order[ key+decimalPlace+'-pre' ] = fn; - - // For HTML types add a search formatter that will strip the HTML - if ( key.match(/^html\-/) ) { - _ext.type.search[ key+decimalPlace ] = _ext.type.search.html; - } + $.extend( true, DataTable.ext.renderer, { + footer: { + _: function ( settings, cell, classes ) { + cell.addClass(classes.tfoot.cell); } - ); - } - - - // Default sort methods - $.extend( _ext.type.order, { - // Dates - "date-pre": function ( d ) { - var ts = Date.parse( d ); - return isNaN(ts) ? -Infinity : ts; - }, - - // html - "html-pre": function ( a ) { - return _empty(a) ? - '' : - a.replace ? - a.replace( /<.*?>/g, "" ).toLowerCase() : - a+''; - }, - - // string - "string-pre": function ( a ) { - // This is a little complex, but faster than always calling toString, - // http://jsperf.com/tostring-v-check - return _empty(a) ? - '' : - typeof a === 'string' ? - a.toLowerCase() : - ! a.toString ? - '' : - a.toString(); - }, - - // string-asc and -desc are retained only for compatibility with the old - // sort methods - "string-asc": function ( x, y ) { - return ((x < y) ? -1 : ((x > y) ? 1 : 0)); }, - "string-desc": function ( x, y ) { - return ((x < y) ? 1 : ((x > y) ? -1 : 0)); - } - } ); + header: { + _: function ( settings, cell, classes ) { + cell.addClass(classes.thead.cell); + if (! settings.oFeatures.bSort) { + cell.addClass(classes.order.none); + } - // Numeric sorting types - order doesn't matter here - _addNumericSort( '' ); + var legacyTop = settings.bSortCellsTop; + var headerRows = cell.closest('thead').find('tr'); + var rowIdx = cell.parent().index(); + // Conditions to not apply the ordering icons + if ( + // Cells and rows which have the attribute to disable the icons + cell.attr('data-dt-order') === 'disable' || + cell.parent().attr('data-dt-order') === 'disable' || + + // Legacy support for `orderCellsTop`. If it is set, then cells + // which are not in the top or bottom row of the header (depending + // on the value) do not get the sorting classes applied to them + (legacyTop === true && rowIdx !== 0) || + (legacyTop === false && rowIdx !== headerRows.length - 1) + ) { + return; + } - $.extend( true, DataTable.ext.renderer, { - header: { - _: function ( settings, cell, column, classes ) { // No additional mark-up required // Attach a sort listener to update on sort - note that using the // `DT` namespace will allow the event to be removed automatically // on destroy, while the `dt` namespaced event is the one we are // listening for - $(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) { + $(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting ) { if ( settings !== ctx ) { // need to check this this is the host return; // table, not a nested one } - var colIdx = column.idx; + var orderClasses = classes.order; + var columns = ctx.api.columns( cell ); + var col = settings.aoColumns[columns.flatten()[0]]; + var orderable = columns.orderable().includes(true); + var ariaType = ''; + var indexes = columns.indexes(); + var sortDirs = columns.orderable(true).flatten(); + var orderedColumns = ',' + sorting.map( function (val) { + return val.col; + } ).join(',') + ','; cell .removeClass( - classes.sSortAsc +' '+ - classes.sSortDesc + orderClasses.isAsc +' '+ + orderClasses.isDesc ) - .addClass( columns[ colIdx ] == 'asc' ? - classes.sSortAsc : columns[ colIdx ] == 'desc' ? - classes.sSortDesc : - column.sSortingClass + .toggleClass( orderClasses.none, ! orderable ) + .toggleClass( orderClasses.canAsc, orderable && sortDirs.includes('asc') ) + .toggleClass( orderClasses.canDesc, orderable && sortDirs.includes('desc') ); + + var sortIdx = orderedColumns.indexOf( ',' + indexes.toArray().join(',') + ',' ); + + if ( sortIdx !== -1 ) { + // Get the ordering direction for the columns under this cell + // Note that it is possible for a cell to be asc and desc sorting + // (column spanning cells) + var orderDirs = columns.order(); + + cell.addClass( + orderDirs.includes('asc') ? orderClasses.isAsc : '' + + orderDirs.includes('desc') ? orderClasses.isDesc : '' ); - } ); - }, + } - jqueryui: function ( settings, cell, column, classes ) { - $('<div/>') - .addClass( classes.sSortJUIWrapper ) - .append( cell.contents() ) - .append( $('<span/>') - .addClass( classes.sSortIcon+' '+column.sSortingClassJUI ) - ) - .appendTo( cell ); + // The ARIA spec says that only one column should be marked with aria-sort + if ( sortIdx === 0 ) { + var firstSort = sorting[0]; + var sortOrder = col.asSorting; - // Attach a sort listener to update on sort - $(settings.nTable).on( 'order.dt.DT', function ( e, ctx, sorting, columns ) { - if ( settings !== ctx ) { - return; + cell.attr('aria-sort', firstSort.dir === 'asc' ? 'ascending' : 'descending'); + + // Determine if the next click will remove sorting or change the sort + ariaType = ! sortOrder[firstSort.index + 1] ? 'Remove' : 'Reverse'; + } + else { + cell.removeAttr('aria-sort'); } - var colIdx = column.idx; + cell.attr('aria-label', orderable + ? col.ariaTitle + ctx.api.i18n('oAria.orderable' + ariaType) + : col.ariaTitle + ); - cell - .removeClass( classes.sSortAsc +" "+classes.sSortDesc ) - .addClass( columns[ colIdx ] == 'asc' ? - classes.sSortAsc : columns[ colIdx ] == 'desc' ? - classes.sSortDesc : - column.sSortingClass - ); + if (orderable) { + cell.find('.dt-column-title').attr('role', 'button'); + cell.attr('tabindex', 0) + } + } ); + } + }, - cell - .find( 'span.'+classes.sSortIcon ) - .removeClass( - classes.sSortJUIAsc +" "+ - classes.sSortJUIDesc +" "+ - classes.sSortJUI +" "+ - classes.sSortJUIAscAllowed +" "+ - classes.sSortJUIDescAllowed - ) - .addClass( columns[ colIdx ] == 'asc' ? - classes.sSortJUIAsc : columns[ colIdx ] == 'desc' ? - classes.sSortJUIDesc : - column.sSortingClassJUI - ); + layout: { + _: function ( settings, container, items ) { + var row = $('<div/>') + .addClass('dt-layout-row') + .appendTo( container ); + + $.each( items, function (key, val) { + var klass = ! val.table ? + 'dt-'+key+' ' : + ''; + + if (val.table) { + row.addClass('dt-layout-table'); + } + + $('<div/>') + .attr({ + id: val.id || null, + "class": 'dt-layout-cell '+klass+(val.className || '') + }) + .append( val.contents ) + .appendTo( row ); } ); } } } ); - /* - * Public helper functions. These aren't used internally by DataTables, or - * called by any of the options passed into DataTables, but they can be used - * externally by developers working with DataTables. They are helper functions - * to make working with DataTables a little bit easier. - */ - var __htmlEscapeEntities = function ( d ) { - if (Array.isArray(d)) { - d = d.join(','); - } + DataTable.feature = {}; - return typeof d === 'string' ? - d - .replace(/&/g, '&amp;') - .replace(/</g, '&lt;') - .replace(/>/g, '&gt;') - .replace(/"/g, '&quot;') : - d; + // Third parameter is internal only! + DataTable.feature.register = function ( name, cb, legacy ) { + DataTable.ext.features[ name ] = cb; + + if (legacy) { + _ext.feature.push({ + cFeature: legacy, + fnInit: cb + }); + } }; - // Common logic for moment, luxon or a date action - function __mld( dt, momentFn, luxonFn, dateFn, arg1 ) { - if (window.moment) { - return dt[momentFn]( arg1 ); + DataTable.feature.register( 'info', function ( settings, opts ) { + // For compatibility with the legacy `info` top level option + if (! settings.oFeatures.bInfo) { + return null; } - else if (window.luxon) { - return dt[luxonFn]( arg1 ); + + var + lang = settings.oLanguage, + tid = settings.sTableId, + n = $('<div/>', { + 'class': settings.oClasses.info.container, + } ); + + opts = $.extend({ + callback: lang.fnInfoCallback, + empty: lang.sInfoEmpty, + postfix: lang.sInfoPostFix, + search: lang.sInfoFiltered, + text: lang.sInfo, + }, opts); + + + // Update display on each draw + settings.aoDrawCallback.push(function (s) { + _fnUpdateInfo(s, opts, n); + }); + + // For the first info display in the table, we add a callback and aria information. + if (! settings._infoEl) { + n.attr({ + 'aria-live': 'polite', + id: tid+'_info', + role: 'status' + }); + + // Table is described by our info div + $(settings.nTable).attr( 'aria-describedby', tid+'_info' ); + + settings._infoEl = n; } - - return dateFn ? dt[dateFn]( arg1 ) : dt; - } + return n; + }, 'i' ); - var __mlWarning = false; - function __mldObj (d, format, locale) { - var dt; + /** + * Update the information elements in the display + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnUpdateInfo ( settings, opts, node ) + { + var + start = settings._iDisplayStart+1, + end = settings.fnDisplayEnd(), + max = settings.fnRecordsTotal(), + total = settings.fnRecordsDisplay(), + out = total + ? opts.text + : opts.empty; - if (window.moment) { - dt = window.moment.utc( d, format, locale, true ); + if ( total !== max ) { + // Record set after filtering + out += ' ' + opts.search; + } - if (! dt.isValid()) { - return null; - } + // Convert the macros + out += opts.postfix; + out = _fnMacros( settings, out ); + + if ( opts.callback ) { + out = opts.callback.call( settings.oInstance, + settings, start, end, max, total, out + ); } - else if (window.luxon) { - dt = format && typeof d === 'string' - ? window.luxon.DateTime.fromFormat( d, format ) - : window.luxon.DateTime.fromISO( d ); - if (! dt.isValid) { - return null; - } + node.html( out ); - dt.setLocale(locale); + _fnCallbackFire(settings, null, 'info', [settings, node[0], out]); + } + + var __searchCounter = 0; + + // opts + // - text + // - placeholder + DataTable.feature.register( 'search', function ( settings, opts ) { + // Don't show the input if filtering isn't available on the table + if (! settings.oFeatures.bFilter) { + return null; } - else if (! format) { - // No format given, must be ISO - dt = new Date(d); + + var classes = settings.oClasses.search; + var tableId = settings.sTableId; + var language = settings.oLanguage; + var previousSearch = settings.oPreviousSearch; + var input = '<input type="search" class="'+classes.input+'"/>'; + + opts = $.extend({ + placeholder: language.sSearchPlaceholder, + text: language.sSearch + }, opts); + + // The _INPUT_ is optional - is appended if not present + if (opts.text.indexOf('_INPUT_') === -1) { + opts.text += '_INPUT_'; } - else { - if (! __mlWarning) { - alert('DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17'); - } - __mlWarning = true; + opts.text = _fnMacros(settings, opts.text); + + // We can put the <input> outside of the label if it is at the start or end + // which helps improve accessability (not all screen readers like implicit + // for elements). + var end = opts.text.match(/_INPUT_$/); + var start = opts.text.match(/^_INPUT_/); + var removed = opts.text.replace(/_INPUT_/, ''); + var str = '<label>' + opts.text + '</label>'; + + if (start) { + str = '_INPUT_<label>' + removed + '</label>'; + } + else if (end) { + str = '<label>' + removed + '</label>_INPUT_'; } - return dt; - } + var filter = $('<div>') + .addClass(classes.container) + .append(str.replace(/_INPUT_/, input)); - // Wrapper for date, datetime and time which all operate the same way with the exception of - // the output string for auto locale support - function __mlHelper (localeString) { - return function ( from, to, locale, def ) { - // Luxon and Moment support - // Argument shifting - if ( arguments.length === 0 ) { - locale = 'en'; - to = null; // means toLocaleString - from = null; // means iso8601 + // add for and id to label and input + filter.find('label').attr('for', 'dt-search-' + __searchCounter); + filter.find('input').attr('id', 'dt-search-' + __searchCounter); + __searchCounter++; + + var searchFn = function(event) { + var val = this.value; + + if(previousSearch.return && event.key !== "Enter") { + return; } - else if ( arguments.length === 1 ) { - locale = 'en'; - to = from; - from = null; + + /* Now do the filter */ + if ( val != previousSearch.search ) { + previousSearch.search = val; + + _fnFilterComplete( settings, previousSearch ); + + // Need to redraw, without resorting + settings._iDisplayStart = 0; + _fnDraw( settings ); } - else if ( arguments.length === 2 ) { - locale = to; - to = from; - from = null; + }; + + var searchDelay = settings.searchDelay !== null ? + settings.searchDelay : + 0; + + var jqFilter = $('input', filter) + .val( previousSearch.search ) + .attr( 'placeholder', opts.placeholder ) + .on( + 'keyup.DT search.DT input.DT paste.DT cut.DT', + searchDelay ? + DataTable.util.debounce( searchFn, searchDelay ) : + searchFn + ) + .on( 'mouseup.DT', function(e) { + // Edge fix! Edge 17 does not trigger anything other than mouse events when clicking + // on the clear icon (Edge bug 17584515). This is safe in other browsers as `searchFn` + // checks the value to see if it has changed. In other browsers it won't have. + setTimeout( function () { + searchFn.call(jqFilter[0], e); + }, 10); + } ) + .on( 'keypress.DT', function(e) { + /* Prevent form submission */ + if ( e.keyCode == 13 ) { + return false; + } + } ) + .attr('aria-controls', tableId); + + // Update the input elements whenever the table is filtered + $(settings.nTable).on( 'search.dt.DT', function ( ev, s ) { + if ( settings === s && jqFilter[0] !== document.activeElement ) { + jqFilter.val( typeof previousSearch.search !== 'function' + ? previousSearch.search + : '' + ); } + } ); - var typeName = 'datetime-' + to; + return filter; + }, 'f' ); - // Add type detection and sorting specific to this date format - we need to be able to identify - // date type columns as such, rather than as numbers in extensions. Hence the need for this. - if (! DataTable.ext.type.order[typeName]) { - // The renderer will give the value to type detect as the type! - DataTable.ext.type.detect.unshift(function (d) { - return d === typeName ? typeName : false; - }); + // opts + // - type - button configuration + // - buttons - number of buttons to show - must be odd + DataTable.feature.register( 'paging', function ( settings, opts ) { + // Don't show the paging input if the table doesn't have paging enabled + if (! settings.oFeatures.bPaginate) { + return null; + } - // The renderer gives us Moment, Luxon or Date obects for the sorting, all of which have a - // `valueOf` which gives milliseconds epoch - DataTable.ext.type.order[typeName + '-asc'] = function (a, b) { - var x = a.valueOf(); - var y = b.valueOf(); + opts = $.extend({ + buttons: DataTable.ext.pager.numbers_length, + type: settings.sPaginationType, + boundaryNumbers: true + }, opts); - return x === y - ? 0 - : x < y - ? -1 - : 1; - } + // To be removed in 2.1 + if (opts.numbers) { + opts.buttons = opts.numbers; + } + + var host = $('<div/>').addClass( settings.oClasses.paging.container + ' paging_' + opts.type ); + var draw = function () { + _pagingDraw(settings, host, opts); + }; - DataTable.ext.type.order[typeName + '-desc'] = function (a, b) { - var x = a.valueOf(); - var y = b.valueOf(); + settings.aoDrawCallback.push(draw); - return x === y - ? 0 - : x > y - ? -1 - : 1; - } + // Responsive redraw of paging control + $(settings.nTable).on('column-sizing.dt.DT', draw); + + return host; + }, 'p' ); + + function _pagingDraw(settings, host, opts) { + if (! settings._bInitComplete) { + return; + } + + var + plugin = DataTable.ext.pager[ opts.type ], + aria = settings.oLanguage.oAria.paginate || {}, + start = settings._iDisplayStart, + len = settings._iDisplayLength, + visRecords = settings.fnRecordsDisplay(), + all = len === -1, + page = all ? 0 : Math.ceil( start / len ), + pages = all ? 1 : Math.ceil( visRecords / len ), + buttons = plugin() + .map(function (val) { + return val === 'numbers' + ? _pagingNumbers(page, pages, opts.buttons, opts.boundaryNumbers) + : val; + }) + .flat(); + + var buttonEls = []; + + for (var i=0 ; i<buttons.length ; i++) { + var button = buttons[i]; + + var btnInfo = _pagingButtonInfo(settings, button, page, pages); + var btn = _fnRenderer( settings, 'pagingButton' )( + settings, + button, + btnInfo.display, + btnInfo.active, + btnInfo.disabled + ); + + // Common attributes + $(btn.clicker).attr({ + 'aria-controls': settings.sTableId, + 'aria-disabled': btnInfo.disabled ? 'true' : null, + 'aria-current': btnInfo.active ? 'page' : null, + 'aria-label': aria[ button ], + 'data-dt-idx': button, + 'tabIndex': btnInfo.disabled ? -1 : settings.iTabIndex, + }); + + if (typeof button !== 'number') { + $(btn.clicker).addClass(button); } - - return function ( d, type ) { - // Allow for a default value - if (d === null || d === undefined) { - if (def === '--now') { - // We treat everything as UTC further down, so no changes are - // made, as such need to get the local date / time as if it were - // UTC - var local = new Date(); - d = new Date( Date.UTC( - local.getFullYear(), local.getMonth(), local.getDate(), - local.getHours(), local.getMinutes(), local.getSeconds() - ) ); - } - else { - d = ''; - } + + _fnBindAction( + btn.clicker, {action: button}, function(e) { + e.preventDefault(); + + _fnPageChange( settings, e.data.action, true ); } + ); + + buttonEls.push(btn.display); + } + + var wrapped = _fnRenderer(settings, 'pagingContainer')( + settings, buttonEls + ); + + var activeEl = host.find(document.activeElement).data('dt-idx'); + + host.empty().append(wrapped); + + if ( activeEl !== undefined ) { + host.find( '[data-dt-idx='+activeEl+']' ).trigger('focus'); + } + + // Responsive - check if the buttons are over two lines based on the + // height of the buttons and the container. + if ( + buttonEls.length && // any buttons + opts.numbers > 1 && // prevent infinite + $(host).height() >= ($(buttonEls[0]).outerHeight() * 2) - 10 + ) { + _pagingDraw(settings, host, $.extend({}, opts, { numbers: opts.numbers - 2 })); + } + } + + /** + * Get properties for a button based on the current paging state of the table + * + * @param {*} settings DT settings object + * @param {*} button The button type in question + * @param {*} page Table's current page + * @param {*} pages Number of pages + * @returns Info object + */ + function _pagingButtonInfo(settings, button, page, pages) { + var lang = settings.oLanguage.oPaginate; + var o = { + display: '', + active: false, + disabled: false + }; - if (type === 'type') { - // Typing uses the type name for fast matching - return typeName; - } + switch ( button ) { + case 'ellipsis': + o.display = '&#x2026;'; + o.disabled = true; + break; - if (d === '') { - return type !== 'sort' - ? '' - : __mldObj('0000-01-01 00:00:00', null, locale); - } + case 'first': + o.display = lang.sFirst; - // Shortcut. If `from` and `to` are the same, we are using the renderer to - // format for ordering, not display - its already in the display format. - if ( to !== null && from === to && type !== 'sort' && type !== 'type' && ! (d instanceof Date) ) { - return d; + if (page === 0) { + o.disabled = true; } + break; - var dt = __mldObj(d, from, locale); + case 'previous': + o.display = lang.sPrevious; - if (dt === null) { - return d; + if ( page === 0 ) { + o.disabled = true; } + break; - if (type === 'sort') { - return dt; - } - - var formatted = to === null - ? __mld(dt, 'toDate', 'toJSDate', '')[localeString]() - : __mld(dt, 'format', 'toFormat', 'toISOString', to); + case 'next': + o.display = lang.sNext; - // XSS protection - return type === 'display' ? - __htmlEscapeEntities( formatted ) : - formatted; - }; - } - } + if ( pages === 0 || page === pages-1 ) { + o.disabled = true; + } + break; - // Based on locale, determine standard number formatting - // Fallback for legacy browsers is US English - var __thousands = ','; - var __decimal = '.'; + case 'last': + o.display = lang.sLast; - if (window.Intl !== undefined) { - try { - var num = new Intl.NumberFormat().formatToParts(100000.1); - - for (var i=0 ; i<num.length ; i++) { - if (num[i].type === 'group') { - __thousands = num[i].value; + if ( pages === 0 || page === pages-1 ) { + o.disabled = true; } - else if (num[i].type === 'decimal') { - __decimal = num[i].value; + break; + + default: + if ( typeof button === 'number' ) { + o.display = settings.fnFormatNumber( button + 1 ); + + if (page === button) { + o.active = true; + } } - } - } - catch (e) { - // noop + break; } + + return o; } - // Formatted date time detection - use by declaring the formats you are going to use - DataTable.datetime = function ( format, locale ) { - var typeName = 'datetime-detect-' + format; + /** + * Compute what number buttons to show in the paging control + * + * @param {*} page Current page + * @param {*} pages Total number of pages + * @param {*} buttons Target number of number buttons + * @param {boolean} addFirstLast Indicate if page 1 and end should be included + * @returns Buttons to show + */ + function _pagingNumbers ( page, pages, buttons, addFirstLast ) { + var + numbers = [], + half = Math.floor(buttons / 2), + before = addFirstLast ? 2 : 1, + after = addFirstLast ? 1 : 0; - if (! locale) { - locale = 'en'; + if ( pages <= buttons ) { + numbers = _range(0, pages); } - - if (! DataTable.ext.type.order[typeName]) { - DataTable.ext.type.detect.unshift(function (d) { - var dt = __mldObj(d, format, locale); - return d === '' || dt ? typeName : false; - }); - - DataTable.ext.type.order[typeName + '-pre'] = function (d) { - return __mldObj(d, format, locale) || 0; + else if (buttons === 1) { + // Single button - current page only + numbers = [page]; + } + else if (buttons === 3) { + // Special logic for just three buttons + if (page <= 1) { + numbers = [0, 1, 'ellipsis']; + } + else if (page >= pages - 2) { + numbers = _range(pages-2, pages); + numbers.unshift('ellipsis'); + } + else { + numbers = ['ellipsis', page, 'ellipsis']; } } - } + else if ( page <= half ) { + numbers = _range(0, buttons-before); + numbers.push('ellipsis'); - /** - * Helpers for `columns.render`. - * - * The options defined here can be used with the `columns.render` initialisation - * option to provide a display renderer. The following functions are defined: - * - * * `number` - Will format numeric data (defined by `columns.data`) for - * display, retaining the original unformatted data for sorting and filtering. - * It takes 5 parameters: - * * `string` - Thousands grouping separator - * * `string` - Decimal point indicator - * * `integer` - Number of decimal points to show - * * `string` (optional) - Prefix. - * * `string` (optional) - Postfix (/suffix). - * * `text` - Escape HTML to help prevent XSS attacks. It has no optional - * parameters. - * - * @example - * // Column definition using the number renderer - * { - * data: "salary", - * render: $.fn.dataTable.render.number( '\'', '.', 0, '$' ) - * } - * - * @namespace - */ - DataTable.render = { - date: __mlHelper('toLocaleDateString'), - datetime: __mlHelper('toLocaleString'), - time: __mlHelper('toLocaleTimeString'), - number: function ( thousands, decimal, precision, prefix, postfix ) { - // Auto locale detection - if (thousands === null || thousands === undefined) { - thousands = __thousands; + if (addFirstLast) { + numbers.push(pages-1); } + } + else if ( page >= pages - 1 - half ) { + numbers = _range(pages-(buttons-before), pages); + numbers.unshift('ellipsis'); - if (decimal === null || decimal === undefined) { - decimal = __decimal; + if (addFirstLast) { + numbers.unshift(0); } + } + else { + numbers = _range(page-half+before, page+half-after); + numbers.push('ellipsis'); + numbers.unshift('ellipsis'); - return { - display: function ( d ) { - if ( typeof d !== 'number' && typeof d !== 'string' ) { - return d; - } + if (addFirstLast) { + numbers.push(pages-1); + numbers.unshift(0); + } + } - if (d === '' || d === null) { - return d; - } + return numbers; + } - var negative = d < 0 ? '-' : ''; - var flo = parseFloat( d ); + var __lengthCounter = 0; - // If NaN then there isn't much formatting that we can do - just - // return immediately, escaping any HTML (this was supposed to - // be a number after all) - if ( isNaN( flo ) ) { - return __htmlEscapeEntities( d ); - } + // opts + // - menu + // - text + DataTable.feature.register( 'pageLength', function ( settings, opts ) { + var features = settings.oFeatures; - flo = flo.toFixed( precision ); - d = Math.abs( flo ); + // For compatibility with the legacy `pageLength` top level option + if (! features.bPaginate || ! features.bLengthChange) { + return null; + } - var intPart = parseInt( d, 10 ); - var floatPart = precision ? - decimal+(d - intPart).toFixed( precision ).substring( 2 ): - ''; + opts = $.extend({ + menu: settings.aLengthMenu, + text: settings.oLanguage.sLengthMenu + }, opts); - // If zero, then can't have a negative prefix - if (intPart === 0 && parseFloat(floatPart) === 0) { - negative = ''; - } + var + classes = settings.oClasses.length, + tableId = settings.sTableId, + menu = opts.menu, + lengths = [], + language = [], + i; - return negative + (prefix||'') + - intPart.toString().replace( - /\B(?=(\d{3})+(?!\d))/g, thousands - ) + - floatPart + - (postfix||''); + // Options can be given in a number of ways + if (Array.isArray( menu[0] )) { + // Old 1.x style - 2D array + lengths = menu[0]; + language = menu[1]; + } + else { + for ( i=0 ; i<menu.length ; i++ ) { + // An object with different label and value + if ($.isPlainObject(menu[i])) { + lengths.push(menu[i].value); + language.push(menu[i].label); } - }; - }, + else { + // Or just a number to display and use + lengths.push(menu[i]); + language.push(menu[i]); + } + } + } - text: function () { - return { - display: __htmlEscapeEntities, - filter: __htmlEscapeEntities - }; + // We can put the <select> outside of the label if it is at the start or + // end which helps improve accessability (not all screen readers like + // implicit for elements). + var end = opts.text.match(/_MENU_$/); + var start = opts.text.match(/^_MENU_/); + var removed = opts.text.replace(/_MENU_/, ''); + var str = '<label>' + opts.text + '</label>'; + + if (start) { + str = '_MENU_<label>' + removed + '</label>'; + } + else if (end) { + str = '<label>' + removed + '</label>_MENU_'; } - }; + // Wrapper element - use a span as a holder for where the select will go + var div = $('<div/>') + .addClass( classes.container ) + .append( + str.replace( '_MENU_', '<span></span>' ) + ); - /* - * This is really a good bit rubbish this method of exposing the internal methods - * publicly... - To be fixed in 2.0 using methods on the prototype - */ + // Save text node content for macro updating + var textNodes = []; + div.find('label')[0].childNodes.forEach(function (el) { + if (el.nodeType === Node.TEXT_NODE) { + textNodes.push({ + el: el, + text: el.textContent + }); + } + }) + // Update the label text in case it has an entries value + var updateEntries = function (len) { + textNodes.forEach(function (node) { + node.el.textContent = _fnMacros(settings, node.text, len); + }); + } - /** - * Create a wrapper function for exporting an internal functions to an external API. - * @param {string} fn API function name - * @returns {function} wrapped function - * @memberof DataTable#internal - */ - function _fnExternApiFunc (fn) - { - return function() { - var args = [_fnSettingsFromNode( this[DataTable.ext.iApiIndex] )].concat( - Array.prototype.slice.call(arguments) + // Next, the select itself, along with the options + var select = $('<select/>', { + 'name': tableId+'_length', + 'aria-controls': tableId, + 'class': classes.select + } ); + + for ( i=0 ; i<lengths.length ; i++ ) { + select[0][ i ] = new Option( + typeof language[i] === 'number' ? + settings.fnFormatNumber( language[i] ) : + language[i], + lengths[i] ); - return DataTable.ext.internal[fn].apply( this, args ); - }; - } + } + // add for and id to label and input + div.find('label').attr('for', 'dt-length-' + __lengthCounter); + select.attr('id', 'dt-length-' + __lengthCounter); + __lengthCounter++; - /** - * Reference to internal functions for use by plug-in developers. Note that - * these methods are references to internal functions and are considered to be - * private. If you use these methods, be aware that they are liable to change - * between versions. - * @namespace - */ - $.extend( DataTable.ext.internal, { - _fnExternApiFunc: _fnExternApiFunc, - _fnBuildAjax: _fnBuildAjax, - _fnAjaxUpdate: _fnAjaxUpdate, - _fnAjaxParameters: _fnAjaxParameters, - _fnAjaxUpdateDraw: _fnAjaxUpdateDraw, - _fnAjaxDataSrc: _fnAjaxDataSrc, - _fnAddColumn: _fnAddColumn, - _fnColumnOptions: _fnColumnOptions, - _fnAdjustColumnSizing: _fnAdjustColumnSizing, - _fnVisibleToColumnIndex: _fnVisibleToColumnIndex, - _fnColumnIndexToVisible: _fnColumnIndexToVisible, - _fnVisbleColumns: _fnVisbleColumns, - _fnGetColumns: _fnGetColumns, - _fnColumnTypes: _fnColumnTypes, - _fnApplyColumnDefs: _fnApplyColumnDefs, - _fnHungarianMap: _fnHungarianMap, - _fnCamelToHungarian: _fnCamelToHungarian, - _fnLanguageCompat: _fnLanguageCompat, - _fnBrowserDetect: _fnBrowserDetect, - _fnAddData: _fnAddData, - _fnAddTr: _fnAddTr, - _fnNodeToDataIndex: _fnNodeToDataIndex, - _fnNodeToColumnIndex: _fnNodeToColumnIndex, - _fnGetCellData: _fnGetCellData, - _fnSetCellData: _fnSetCellData, - _fnSplitObjNotation: _fnSplitObjNotation, - _fnGetObjectDataFn: _fnGetObjectDataFn, - _fnSetObjectDataFn: _fnSetObjectDataFn, - _fnGetDataMaster: _fnGetDataMaster, - _fnClearTable: _fnClearTable, - _fnDeleteIndex: _fnDeleteIndex, - _fnInvalidate: _fnInvalidate, - _fnGetRowElements: _fnGetRowElements, - _fnCreateTr: _fnCreateTr, - _fnBuildHead: _fnBuildHead, - _fnDrawHead: _fnDrawHead, - _fnDraw: _fnDraw, - _fnReDraw: _fnReDraw, - _fnAddOptionsHtml: _fnAddOptionsHtml, - _fnDetectHeader: _fnDetectHeader, - _fnGetUniqueThs: _fnGetUniqueThs, - _fnFeatureHtmlFilter: _fnFeatureHtmlFilter, - _fnFilterComplete: _fnFilterComplete, - _fnFilterCustom: _fnFilterCustom, - _fnFilterColumn: _fnFilterColumn, - _fnFilter: _fnFilter, - _fnFilterCreateSearch: _fnFilterCreateSearch, - _fnEscapeRegex: _fnEscapeRegex, - _fnFilterData: _fnFilterData, - _fnFeatureHtmlInfo: _fnFeatureHtmlInfo, - _fnUpdateInfo: _fnUpdateInfo, - _fnInfoMacros: _fnInfoMacros, - _fnInitialise: _fnInitialise, - _fnInitComplete: _fnInitComplete, - _fnLengthChange: _fnLengthChange, - _fnFeatureHtmlLength: _fnFeatureHtmlLength, - _fnFeatureHtmlPaginate: _fnFeatureHtmlPaginate, - _fnPageChange: _fnPageChange, - _fnFeatureHtmlProcessing: _fnFeatureHtmlProcessing, - _fnProcessingDisplay: _fnProcessingDisplay, - _fnFeatureHtmlTable: _fnFeatureHtmlTable, - _fnScrollDraw: _fnScrollDraw, - _fnApplyToChildren: _fnApplyToChildren, - _fnCalculateColumnWidths: _fnCalculateColumnWidths, - _fnThrottle: _fnThrottle, - _fnConvertToWidth: _fnConvertToWidth, - _fnGetWidestNode: _fnGetWidestNode, - _fnGetMaxLenString: _fnGetMaxLenString, - _fnStringToCss: _fnStringToCss, - _fnSortFlatten: _fnSortFlatten, - _fnSort: _fnSort, - _fnSortAria: _fnSortAria, - _fnSortListener: _fnSortListener, - _fnSortAttachListener: _fnSortAttachListener, - _fnSortingClasses: _fnSortingClasses, - _fnSortData: _fnSortData, - _fnSaveState: _fnSaveState, - _fnLoadState: _fnLoadState, - _fnImplementState: _fnImplementState, - _fnSettingsFromNode: _fnSettingsFromNode, - _fnLog: _fnLog, - _fnMap: _fnMap, - _fnBindAction: _fnBindAction, - _fnCallbackReg: _fnCallbackReg, - _fnCallbackFire: _fnCallbackFire, - _fnLengthOverflow: _fnLengthOverflow, - _fnRenderer: _fnRenderer, - _fnDataSource: _fnDataSource, - _fnRowAttributes: _fnRowAttributes, - _fnExtend: _fnExtend, - _fnCalculateEnd: function () {} // Used by a lot of plug-ins, but redundant - // in 1.10, so this dead-end function is - // added to prevent errors - } ); + // Swap in the select list + div.find('span').replaceWith(select); + + // Can't use `select` variable as user might provide their own and the + // reference is broken by the use of outerHTML + $('select', div) + .val( settings._iDisplayLength ) + .on( 'change.DT', function() { + _fnLengthChange( settings, $(this).val() ); + _fnDraw( settings ); + } ); + + // Update node value whenever anything changes the table's length + $(settings.nTable).on( 'length.dt.DT', function (e, s, len) { + if ( settings === s ) { + $('select', div).val( len ); + + // Resolve plurals in the text for the new length + updateEntries(len); + } + } ); + + updateEntries(settings._iDisplayLength); + return div; + }, 'l' ); // jQuery access $.fn.dataTable = DataTable; @@ -15724,7 +13184,7 @@ /*! DataTables Bootstrap 5 integration - * 2020 SpryMedia Ltd - datatables.net/license + * © SpryMedia Ltd - datatables.net/license */ (function( factory ){ @@ -15768,168 +13228,103 @@ // Browser factory( jQuery, window, document ); } -}(function( $, window, document, undefined ) { +}(function( $, window, document ) { 'use strict'; var DataTable = $.fn.dataTable; /** - * DataTables integration for Bootstrap 5. This requires Bootstrap 5 and - * DataTables 1.10 or newer. + * DataTables integration for Bootstrap 5. * * This file sets the defaults and adds options to DataTables to style its - * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap + * controls using Bootstrap. See https://datatables.net/manual/styling/bootstrap * for further information. */ /* Set the defaults for DataTables initialisation */ $.extend( true, DataTable.defaults, { - dom: - "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + - "<'row dt-row'<'col-sm-12'tr>>" + - "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", renderer: 'bootstrap' } ); /* Default class modification */ -$.extend( DataTable.ext.classes, { - sWrapper: "dataTables_wrapper dt-bootstrap5", - sFilterInput: "form-control form-control-sm", - sLengthSelect: "form-select form-select-sm", - sProcessing: "dataTables_processing card", - sPageButton: "paginate_button page-item" +$.extend( true, DataTable.ext.classes, { + container: "dt-container dt-bootstrap5", + search: { + input: "form-control form-control-sm" + }, + length: { + select: "form-select form-select-sm" + }, + processing: { + container: "dt-processing card" + } } ); /* Bootstrap paging button renderer */ -DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { - var api = new DataTable.Api( settings ); - var classes = settings.oClasses; - var lang = settings.oLanguage.oPaginate; - var aria = settings.oLanguage.oAria.paginate || {}; - var btnDisplay, btnClass; - - var attach = function( container, buttons ) { - var i, ien, node, button; - var clickHandler = function ( e ) { - e.preventDefault(); - if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { - api.page( e.data.action ).draw( 'page' ); - } - }; - - for ( i=0, ien=buttons.length ; i<ien ; i++ ) { - button = buttons[i]; - - if ( Array.isArray( button ) ) { - attach( container, button ); - } - else { - btnDisplay = ''; - btnClass = ''; - - switch ( button ) { - case 'ellipsis': - btnDisplay = '&#x2026;'; - btnClass = 'disabled'; - break; - - case 'first': - btnDisplay = lang.sFirst; - btnClass = button + (page > 0 ? - '' : ' disabled'); - break; - - case 'previous': - btnDisplay = lang.sPrevious; - btnClass = button + (page > 0 ? - '' : ' disabled'); - break; +DataTable.ext.renderer.pagingButton.bootstrap = function (settings, buttonType, content, active, disabled) { + var btnClasses = ['dt-paging-button', 'page-item']; - case 'next': - btnDisplay = lang.sNext; - btnClass = button + (page < pages-1 ? - '' : ' disabled'); - break; - - case 'last': - btnDisplay = lang.sLast; - btnClass = button + (page < pages-1 ? - '' : ' disabled'); - break; - - default: - btnDisplay = button + 1; - btnClass = page === button ? - 'active' : ''; - break; - } + if (active) { + btnClasses.push('active'); + } - if ( btnDisplay ) { - var disabled = btnClass.indexOf('disabled') !== -1; + if (disabled) { + btnClasses.push('disabled') + } - node = $('<li>', { - 'class': classes.sPageButton+' '+btnClass, - 'id': idx === 0 && typeof button === 'string' ? - settings.sTableId +'_'+ button : - null - } ) - .append( $('<a>', { - 'href': disabled ? null : '#', - 'aria-controls': settings.sTableId, - 'aria-disabled': disabled ? 'true' : null, - 'aria-label': aria[ button ], - 'role': 'link', - 'aria-current': btnClass === 'active' ? 'page' : null, - 'data-dt-idx': button, - 'tabindex': disabled ? -1 : settings.iTabIndex, - 'class': 'page-link' - } ) - .html( btnDisplay ) - ) - .appendTo( container ); + var li = $('<li>').addClass(btnClasses.join(' ')); + var a = $('<a>', { + 'href': disabled ? null : '#', + 'class': 'page-link' + }) + .html(content) + .appendTo(li); - settings.oApi._fnBindAction( - node, {action: button}, clickHandler - ); - } - } - } + return { + display: li, + clicker: a }; +}; - var hostEl = $(host); - // IE9 throws an 'unknown error' if document.activeElement is used - // inside an iframe or frame. - var activeEl; - - try { - // Because this approach is destroying and recreating the paging - // elements, focus is lost on the select button which is bad for - // accessibility. So we want to restore focus once the draw has - // completed - activeEl = hostEl.find(document.activeElement).data('dt-idx'); - } - catch (e) {} +DataTable.ext.renderer.pagingContainer.bootstrap = function (settings, buttonEls) { + return $('<ul/>').addClass('pagination').append(buttonEls); +}; - var paginationEl = hostEl.children('ul.pagination'); +DataTable.ext.renderer.layout.bootstrap = function ( settings, container, items ) { + var row = $( '<div/>', { + "class": items.full ? + 'row mt-2 justify-content-md-center' : + 'row mt-2 justify-content-between' + } ) + .appendTo( container ); - if (paginationEl.length) { - paginationEl.empty(); - } - else { - paginationEl = hostEl.html('<ul/>').children('ul').addClass('pagination'); - } + $.each( items, function (key, val) { + var klass; - attach( - paginationEl, - buttons - ); + // Apply start / end (left / right when ltr) margins + if (val.table) { + klass = 'col-12'; + } + else if (key === 'start') { + klass = 'col-md-auto me-auto'; + } + else if (key === 'end') { + klass = 'col-md-auto ms-auto'; + } + else { + klass = 'col-md'; + } - if ( activeEl !== undefined ) { - hostEl.find('[data-dt-idx='+activeEl+']').trigger('focus'); - } + $( '<div/>', { + id: val.id || null, + "class": klass + ' ' + (val.className || '') + } ) + .append( val.contents ) + .appendTo( row ); + } ); }; diff --git a/src/static/scripts/jdenticon-3.3.0.js b/src/static/scripts/jdenticon-3.3.0.js @@ -0,0 +1,1507 @@ +/** + * Jdenticon 3.3.0 + * http://jdenticon.com + * + * Built: 2024-05-10T09:48:41.921Z + * + * MIT License + * + * Copyright (c) 2014-2024 Daniel Mester Pirttijärvi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +(function (umdGlobal, factory) { + var jdenticon = factory(umdGlobal); + + // Node.js + if (typeof module !== "undefined" && "exports" in module) { + module["exports"] = jdenticon; + } + // RequireJS + else if (typeof define === "function" && define["amd"]) { + define([], function () { return jdenticon; }); + } + // No module loader + else { + umdGlobal["jdenticon"] = jdenticon; + } +})(typeof self !== "undefined" ? self : this, function (umdGlobal) { +'use strict'; + +/** + * Parses a substring of the hash as a number. + * @param {number} startPosition + * @param {number=} octets + */ +function parseHex(hash, startPosition, octets) { + return parseInt(hash.substr(startPosition, octets), 16); +} + +function decToHex(v) { + v |= 0; // Ensure integer value + return v < 0 ? "00" : + v < 16 ? "0" + v.toString(16) : + v < 256 ? v.toString(16) : + "ff"; +} + +function hueToRgb(m1, m2, h) { + h = h < 0 ? h + 6 : h > 6 ? h - 6 : h; + return decToHex(255 * ( + h < 1 ? m1 + (m2 - m1) * h : + h < 3 ? m2 : + h < 4 ? m1 + (m2 - m1) * (4 - h) : + m1)); +} + +/** + * @param {string} color Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported. + * @returns {string} + */ +function parseColor(color) { + if (/^#[0-9a-f]{3,8}$/i.test(color)) { + var result; + var colorLength = color.length; + + if (colorLength < 6) { + var r = color[1], + g = color[2], + b = color[3], + a = color[4] || ""; + result = "#" + r + r + g + g + b + b + a + a; + } + if (colorLength == 7 || colorLength > 8) { + result = color; + } + + return result; + } +} + +/** + * Converts a hexadecimal color to a CSS3 compatible color. + * @param {string} hexColor Color on the format "#RRGGBB" or "#RRGGBBAA" + * @returns {string} + */ +function toCss3Color(hexColor) { + var a = parseHex(hexColor, 7, 2); + var result; + + if (isNaN(a)) { + result = hexColor; + } else { + var r = parseHex(hexColor, 1, 2), + g = parseHex(hexColor, 3, 2), + b = parseHex(hexColor, 5, 2); + result = "rgba(" + r + "," + g + "," + b + "," + (a / 255).toFixed(2) + ")"; + } + + return result; +} + +/** + * Converts an HSL color to a hexadecimal RGB color. + * @param {number} hue Hue in range [0, 1] + * @param {number} saturation Saturation in range [0, 1] + * @param {number} lightness Lightness in range [0, 1] + * @returns {string} + */ +function hsl(hue, saturation, lightness) { + // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color + var result; + + if (saturation == 0) { + var partialHex = decToHex(lightness * 255); + result = partialHex + partialHex + partialHex; + } + else { + var m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation, + m1 = lightness * 2 - m2; + result = + hueToRgb(m1, m2, hue * 6 + 2) + + hueToRgb(m1, m2, hue * 6) + + hueToRgb(m1, m2, hue * 6 - 2); + } + + return "#" + result; +} + +/** + * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues + * @param {number} hue Hue in range [0, 1] + * @param {number} saturation Saturation in range [0, 1] + * @param {number} lightness Lightness in range [0, 1] + * @returns {string} + */ +function correctedHsl(hue, saturation, lightness) { + // The corrector specifies the perceived middle lightness for each hue + var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], + corrector = correctors[(hue * 6 + 0.5) | 0]; + + // Adjust the input lightness relative to the corrector + lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2; + + return hsl(hue, saturation, lightness); +} + +/* global umdGlobal */ + +// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for +// backward compatibility. +var GLOBAL = umdGlobal; + +/** + * @typedef {Object} ParsedConfiguration + * @property {number} colorSaturation + * @property {number} grayscaleSaturation + * @property {string} backColor + * @property {number} iconPadding + * @property {function(number):number} hue + * @property {function(number):number} colorLightness + * @property {function(number):number} grayscaleLightness + */ + +var CONFIG_PROPERTIES = { + G/*GLOBAL*/: "jdenticon_config", + n/*MODULE*/: "config", +}; + +var rootConfigurationHolder = {}; + +/** + * Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console + * when it is being used. + * @param {!Object} rootObject + */ +function defineConfigProperty(rootObject) { + rootConfigurationHolder = rootObject; +} + +/** + * Sets a new icon style configuration. The new configuration is not merged with the previous one. * + * @param {Object} newConfiguration - New configuration object. + */ +function configure(newConfiguration) { + if (arguments.length) { + rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] = newConfiguration; + } + return rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/]; +} + +/** + * Gets the normalized current Jdenticon color configuration. Missing fields have default values. + * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A + * local configuration overrides the global configuration in it entirety. This parameter can for backward + * compatibility also contain a padding value. A padding value only overrides the global padding, not the + * entire global configuration. + * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor + * explicitly to the API method. + * @returns {ParsedConfiguration} + */ +function getConfiguration(paddingOrLocalConfig, defaultPadding) { + var configObject = + typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig || + rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] || + GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || + { }, + + lightnessConfig = configObject["lightness"] || { }, + + // In versions < 2.1.0 there was no grayscale saturation - + // saturation was the color saturation. + saturation = configObject["saturation"] || { }, + colorSaturation = "color" in saturation ? saturation["color"] : saturation, + grayscaleSaturation = saturation["grayscale"], + + backColor = configObject["backColor"], + padding = configObject["padding"]; + + /** + * Creates a lightness range. + */ + function lightness(configName, defaultRange) { + var range = lightnessConfig[configName]; + + // Check if the lightness range is an array-like object. This way we ensure the + // array contain two values at the same time. + if (!(range && range.length > 1)) { + range = defaultRange; + } + + /** + * Gets a lightness relative the specified value in the specified lightness range. + */ + return function (value) { + value = range[0] + value * (range[1] - range[0]); + return value < 0 ? 0 : value > 1 ? 1 : value; + }; + } + + /** + * Gets a hue allowed by the configured hue restriction, + * provided the originally computed hue. + */ + function hueFunction(originalHue) { + var hueConfig = configObject["hues"]; + var hue; + + // Check if 'hues' is an array-like object. This way we also ensure that + // the array is not empty, which would mean no hue restriction. + if (hueConfig && hueConfig.length > 0) { + // originalHue is in the range [0, 1] + // Multiply with 0.999 to change the range to [0, 1) and then truncate the index. + hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)]; + } + + return typeof hue == "number" ? + + // A hue was specified. We need to convert the hue from + // degrees on any turn - e.g. 746° is a perfectly valid hue - + // to turns in the range [0, 1). + ((((hue / 360) % 1) + 1) % 1) : + + // No hue configured => use original hue + originalHue; + } + + return { + X/*hue*/: hueFunction, + p/*colorSaturation*/: typeof colorSaturation == "number" ? colorSaturation : 0.5, + H/*grayscaleSaturation*/: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0, + q/*colorLightness*/: lightness("color", [0.4, 0.8]), + I/*grayscaleLightness*/: lightness("grayscale", [0.3, 0.9]), + J/*backColor*/: parseColor(backColor), + Y/*iconPadding*/: + typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig : + typeof padding == "number" ? padding : + defaultPadding + } +} + +var ICON_TYPE_SVG = 1; + +var ICON_TYPE_CANVAS = 2; + +var ATTRIBUTES = { + t/*HASH*/: "data-jdenticon-hash", + o/*VALUE*/: "data-jdenticon-value" +}; + +var IS_RENDERED_PROPERTY = "jdenticonRendered"; + +var ICON_SELECTOR = "[" + ATTRIBUTES.t/*HASH*/ +"],[" + ATTRIBUTES.o/*VALUE*/ +"]"; + +var documentQuerySelectorAll = /** @type {!Function} */ ( + typeof document !== "undefined" && document.querySelectorAll.bind(document)); + +function getIdenticonType(el) { + if (el) { + var tagName = el["tagName"]; + + if (/^svg$/i.test(tagName)) { + return ICON_TYPE_SVG; + } + + if (/^canvas$/i.test(tagName) && "getContext" in el) { + return ICON_TYPE_CANVAS; + } + } +} + +function whenDocumentIsReady(/** @type {Function} */ callback) { + function loadedHandler() { + document.removeEventListener("DOMContentLoaded", loadedHandler); + window.removeEventListener("load", loadedHandler); + setTimeout(callback, 0); // Give scripts a chance to run + } + + if (typeof document !== "undefined" && + typeof window !== "undefined" && + typeof setTimeout !== "undefined" + ) { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", loadedHandler); + window.addEventListener("load", loadedHandler); + } else { + // Document already loaded. The load events above likely won't be raised + setTimeout(callback, 0); + } + } +} + +function observer(updateCallback) { + if (typeof MutationObserver != "undefined") { + var mutationObserver = new MutationObserver(function onmutation(mutations) { + for (var mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) { + var mutation = mutations[mutationIndex]; + var addedNodes = mutation.addedNodes; + + for (var addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) { + var addedNode = addedNodes[addedNodeIndex]; + + // Skip other types of nodes than element nodes, since they might not support + // the querySelectorAll method => runtime error. + if (addedNode.nodeType == 1) { + if (getIdenticonType(addedNode)) { + updateCallback(addedNode); + } + else { + var icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR); + for (var iconIndex = 0; iconIndex < icons.length; iconIndex++) { + updateCallback(icons[iconIndex]); + } + } + } + } + + if (mutation.type == "attributes" && getIdenticonType(mutation.target)) { + updateCallback(mutation.target); + } + } + }); + + mutationObserver.observe(document.body, { + "childList": true, + "attributes": true, + "attributeFilter": [ATTRIBUTES.o/*VALUE*/, ATTRIBUTES.t/*HASH*/, "width", "height"], + "subtree": true, + }); + } +} + +/** + * Represents a point. + */ +function Point(x, y) { + this.x = x; + this.y = y; +} + +/** + * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, + * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. + */ +function Transform(x, y, size, rotation) { + this.u/*_x*/ = x; + this.v/*_y*/ = y; + this.K/*_size*/ = size; + this.Z/*_rotation*/ = rotation; +} + +/** + * Transforms the specified point based on the translation and rotation specification for this Transform. + * @param {number} x x-coordinate + * @param {number} y y-coordinate + * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + */ +Transform.prototype.L/*transformIconPoint*/ = function transformIconPoint (x, y, w, h) { + var right = this.u/*_x*/ + this.K/*_size*/, + bottom = this.v/*_y*/ + this.K/*_size*/, + rotation = this.Z/*_rotation*/; + return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) : + rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : + rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) : + new Point(this.u/*_x*/ + x, this.v/*_y*/ + y); +}; + +var NO_TRANSFORM = new Transform(0, 0, 0, 0); + + + +/** + * Provides helper functions for rendering common basic shapes. + */ +function Graphics(renderer) { + /** + * @type {Renderer} + * @private + */ + this.M/*_renderer*/ = renderer; + + /** + * @type {Transform} + */ + this.A/*currentTransform*/ = NO_TRANSFORM; +} +var Graphics__prototype = Graphics.prototype; + +/** + * Adds a polygon to the underlying renderer. + * @param {Array<number>} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] + * @param {boolean=} invert Specifies if the polygon will be inverted. + */ +Graphics__prototype.g/*addPolygon*/ = function addPolygon (points, invert) { + var this$1 = this; + + var di = invert ? -2 : 2, + transformedPoints = []; + + for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { + transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1])); + } + + this.M/*_renderer*/.g/*addPolygon*/(transformedPoints); +}; + +/** + * Adds a polygon to the underlying renderer. + * Source: http://stackoverflow.com/a/2173084 + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} size The size of the ellipse. + * @param {boolean=} invert Specifies if the ellipse will be inverted. + */ +Graphics__prototype.h/*addCircle*/ = function addCircle (x, y, size, invert) { + var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size); + this.M/*_renderer*/.h/*addCircle*/(p, size, invert); +}; + +/** + * Adds a rectangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle. + * @param {number} w The width of the rectangle. + * @param {number} h The height of the rectangle. + * @param {boolean=} invert Specifies if the rectangle will be inverted. + */ +Graphics__prototype.i/*addRectangle*/ = function addRectangle (x, y, w, h, invert) { + this.g/*addPolygon*/([ + x, y, + x + w, y, + x + w, y + h, + x, y + h + ], invert); +}; + +/** + * Adds a right triangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} w The width of the triangle. + * @param {number} h The height of the triangle. + * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. + * @param {boolean=} invert Specifies if the triangle will be inverted. + */ +Graphics__prototype.j/*addTriangle*/ = function addTriangle (x, y, w, h, r, invert) { + var points = [ + x + w, y, + x + w, y + h, + x, y + h, + x, y + ]; + points.splice(((r || 0) % 4) * 2, 2); + this.g/*addPolygon*/(points, invert); +}; + +/** + * Adds a rhombus to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} w The width of the rhombus. + * @param {number} h The height of the rhombus. + * @param {boolean=} invert Specifies if the rhombus will be inverted. + */ +Graphics__prototype.N/*addRhombus*/ = function addRhombus (x, y, w, h, invert) { + this.g/*addPolygon*/([ + x + w / 2, y, + x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2 + ], invert); +}; + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + * @param {number} positionIndex + */ +function centerShape(index, g, cell, positionIndex) { + index = index % 14; + + var k, m, w, h, inner, outer; + + !index ? ( + k = cell * 0.42, + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell - k * 2, + cell - k, cell, + 0, cell + ])) : + + index == 1 ? ( + w = 0 | (cell * 0.5), + h = 0 | (cell * 0.8), + + g.j/*addTriangle*/(cell - w, 0, w, h, 2)) : + + index == 2 ? ( + w = 0 | (cell / 3), + g.i/*addRectangle*/(w, w, cell - w, cell - w)) : + + index == 3 ? ( + inner = cell * 0.1, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 6 ? 1 : + cell < 8 ? 2 : + (0 | (cell * 0.25)), + + inner = + inner > 1 ? (0 | inner) : // large icon => truncate decimals + inner > 0.5 ? 1 : // medium size icon => fixed width + inner, // small icon => anti-aliased border + + g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) : + + index == 4 ? ( + m = 0 | (cell * 0.15), + w = 0 | (cell * 0.5), + g.h/*addCircle*/(cell - w - m, cell - w - m, w)) : + + index == 5 ? ( + inner = cell * 0.1, + outer = inner * 4, + + // Align edge to nearest pixel in large icons + outer > 3 && (outer = 0 | outer), + + g.i/*addRectangle*/(0, 0, cell, cell), + g.g/*addPolygon*/([ + outer, outer, + cell - inner, outer, + outer + (cell - outer - inner) / 2, cell - inner + ], true)) : + + index == 6 ? + g.g/*addPolygon*/([ + 0, 0, + cell, 0, + cell, cell * 0.7, + cell * 0.4, cell * 0.4, + cell * 0.7, cell, + 0, cell + ]) : + + index == 7 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 8 ? ( + g.i/*addRectangle*/(0, 0, cell, cell / 2), + g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2), + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) : + + index == 9 ? ( + inner = cell * 0.14, + // Use fixed outer border widths in small icons to ensure the border is drawn + outer = + cell < 4 ? 1 : + cell < 6 ? 2 : + (0 | (cell * 0.35)), + + inner = + cell < 8 ? inner : // small icon => anti-aliased border + (0 | inner), // large icon => truncate decimals + + g.i/*addRectangle*/(0, 0, cell, cell), + g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) : + + index == 10 ? ( + inner = cell * 0.12, + outer = inner * 3, + + g.i/*addRectangle*/(0, 0, cell, cell), + g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) : + + index == 11 ? + g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : + + index == 12 ? ( + m = cell * 0.25, + g.i/*addRectangle*/(0, 0, cell, cell), + g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) : + + // 13 + ( + !positionIndex && ( + m = cell * 0.4, w = cell * 1.2, + g.h/*addCircle*/(m, m, w) + ) + ); +} + +/** + * @param {number} index + * @param {Graphics} g + * @param {number} cell + */ +function outerShape(index, g, cell) { + index = index % 4; + + var m; + + !index ? + g.j/*addTriangle*/(0, 0, cell, cell, 0) : + + index == 1 ? + g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) : + + index == 2 ? + g.N/*addRhombus*/(0, 0, cell, cell) : + + // 3 + ( + m = cell / 6, + g.h/*addCircle*/(m, m, cell - 2 * m) + ); +} + +/** + * Gets a set of identicon color candidates for a specified hue and config. + * @param {number} hue + * @param {ParsedConfiguration} config + */ +function colorTheme(hue, config) { + hue = config.X/*hue*/(hue); + return [ + // Dark gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)), + // Mid color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)), + // Light gray + correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)), + // Light color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)), + // Dark color + correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0)) + ]; +} + +/** + * Draws an identicon to a specified renderer. + * @param {Renderer} renderer + * @param {string} hash + * @param {Object|number=} config + */ +function iconGenerator(renderer, hash, config) { + var parsedConfig = getConfiguration(config, 0.08); + + // Set background color + if (parsedConfig.J/*backColor*/) { + renderer.m/*setBackground*/(parsedConfig.J/*backColor*/); + } + + // Calculate padding and round to nearest integer + var size = renderer.k/*iconSize*/; + var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0; + size -= padding * 2; + + var graphics = new Graphics(renderer); + + // Calculate cell size and ensure it is an integer + var cell = 0 | (size / 4); + + // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon + var x = 0 | (padding + size / 2 - cell * 2); + var y = 0 | (padding + size / 2 - cell * 2); + + function renderShape(colorIndex, shapes, index, rotationIndex, positions) { + var shapeIndex = parseHex(hash, index, 1); + var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0; + + renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]); + + for (var i = 0; i < positions.length; i++) { + graphics.A/*currentTransform*/ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4); + shapes(shapeIndex, graphics, cell, i); + } + + renderer.P/*endShape*/(); + } + + // AVAILABLE COLORS + var hue = parseHex(hash, -7) / 0xfffffff, + + // Available colors for this icon + availableColors = colorTheme(hue, parsedConfig), + + // The index of the selected colors + selectedColorIndexes = []; + + var index; + + function isDuplicate(values) { + if (values.indexOf(index) >= 0) { + for (var i = 0; i < values.length; i++) { + if (selectedColorIndexes.indexOf(values[i]) >= 0) { + return true; + } + } + } + } + + for (var i = 0; i < 3; i++) { + index = parseHex(hash, 8 + i, 1) % availableColors.length; + if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo + isDuplicate([2, 3])) { // Disallow light gray and light color combo + index = 1; + } + selectedColorIndexes.push(index); + } + + // ACTUAL RENDERING + // Sides + renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); + // Corners + renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); + // Center + renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); + + renderer.finish(); +} + +/** + * Computes a SHA1 hash for any value and returns it as a hexadecimal string. + * + * This function is optimized for minimal code size and rather short messages. + * + * @param {string} message + */ +function sha1(message) { + var HASH_SIZE_HALF_BYTES = 40; + var BLOCK_SIZE_WORDS = 16; + + // Variables + // `var` is used to be able to minimize the number of `var` keywords. + var i = 0, + f = 0, + + // Use `encodeURI` to UTF8 encode the message without any additional libraries + // We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky + // since `unescape` is deprecated. + urlEncodedMessage = encodeURI(message) + "%80", // trailing '1' bit padding + + // This can be changed to a preallocated Uint32Array array for greater performance and larger code size + data = [], + dataSize, + + hashBuffer = [], + + a = 0x67452301, + b = 0xefcdab89, + c = ~a, + d = ~b, + e = 0xc3d2e1f0, + hash = [a, b, c, d, e], + + blockStartIndex = 0, + hexHash = ""; + + /** + * Rotates the value a specified number of bits to the left. + * @param {number} value Value to rotate + * @param {number} shift Bit count to shift. + */ + function rotl(value, shift) { + return (value << shift) | (value >>> (32 - shift)); + } + + // Message data + for ( ; i < urlEncodedMessage.length; f++) { + data[f >> 2] = data[f >> 2] | + ( + ( + urlEncodedMessage[i] == "%" + // Percent encoded byte + ? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16) + // Unencoded byte + : urlEncodedMessage.charCodeAt(i++) + ) + + // Read bytes in reverse order (big endian words) + << ((3 - (f & 3)) * 8) + ); + } + + // f is now the length of the utf8 encoded message + // 7 = 8 bytes (64 bit) for message size, -1 to round down + // >> 6 = integer division with block size + dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS; + + // Message size in bits. + // SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least + // significant 32 bits are set. -8 is for the '1' bit padding byte. + data[dataSize - 1] = f * 8 - 8; + + // Compute hash + for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) { + for (i = 0; i < 80; i++) { + f = rotl(a, 5) + e + ( + // Ch + i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 : + + // Parity + i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 : + + // Maj + i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc : + + // Parity + (b ^ c ^ d) + 0xca62c1d6 + ) + ( + hashBuffer[i] = i < BLOCK_SIZE_WORDS + // Bitwise OR is used to coerse `undefined` to 0 + ? (data[blockStartIndex + i] | 0) + : rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1) + ); + + e = d; + d = c; + c = rotl(b, 30); + b = a; + a = f; + } + + hash[0] = a = ((hash[0] + a) | 0); + hash[1] = b = ((hash[1] + b) | 0); + hash[2] = c = ((hash[2] + c) | 0); + hash[3] = d = ((hash[3] + d) | 0); + hash[4] = e = ((hash[4] + e) | 0); + } + + // Format hex hash + for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) { + hexHash += ( + ( + // Get word (2^3 half-bytes per word) + hash[i >> 3] >>> + + // Append half-bytes in reverse order + ((7 - (i & 7)) * 4) + ) + // Clamp to half-byte + & 0xf + ).toString(16); + } + + return hexHash; +} + +/** + * Inputs a value that might be a valid hash string for Jdenticon and returns it + * if it is determined valid, otherwise a falsy value is returned. + */ +function isValidHash(hashCandidate) { + return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate; +} + +/** + * Computes a hash for the specified value. Currently SHA1 is used. This function + * always returns a valid hash. + */ +function computeHash(value) { + return sha1(value == null ? "" : "" + value); +} + + + +/** + * Renderer redirecting drawing commands to a canvas context. + * @implements {Renderer} + */ +function CanvasRenderer(ctx, iconSize) { + var canvas = ctx.canvas; + var width = canvas.width; + var height = canvas.height; + + ctx.save(); + + if (!iconSize) { + iconSize = Math.min(width, height); + + ctx.translate( + ((width - iconSize) / 2) | 0, + ((height - iconSize) / 2) | 0); + } + + /** + * @private + */ + this.l/*_ctx*/ = ctx; + this.k/*iconSize*/ = iconSize; + + ctx.clearRect(0, 0, iconSize, iconSize); +} +var CanvasRenderer__prototype = CanvasRenderer.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb[aa]. + */ +CanvasRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { + var ctx = this.l/*_ctx*/; + var iconSize = this.k/*iconSize*/; + + ctx.fillStyle = toCss3Color(fillColor); + ctx.fillRect(0, 0, iconSize, iconSize); +}; + +/** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} fillColor Fill color on format #rrggbb[aa]. + */ +CanvasRenderer__prototype.O/*beginShape*/ = function beginShape (fillColor) { + var ctx = this.l/*_ctx*/; + ctx.fillStyle = toCss3Color(fillColor); + ctx.beginPath(); +}; + +/** + * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas. + */ +CanvasRenderer__prototype.P/*endShape*/ = function endShape () { + this.l/*_ctx*/.fill(); +}; + +/** + * Adds a polygon to the rendering queue. + * @param points An array of Point objects. + */ +CanvasRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { + var ctx = this.l/*_ctx*/; + ctx.moveTo(points[0].x, points[0].y); + for (var i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y); + } + ctx.closePath(); +}; + +/** + * Adds a circle to the rendering queue. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +CanvasRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + var ctx = this.l/*_ctx*/, + radius = diameter / 2; + ctx.moveTo(point.x + radius, point.y + radius); + ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise); + ctx.closePath(); +}; + +/** + * Called when the icon has been completely drawn. + */ +CanvasRenderer__prototype.finish = function finish () { + this.l/*_ctx*/.restore(); +}; + +/** + * Draws an identicon to a context. + * @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0). + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. + * @param {number} size - Icon size in pixels. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function drawIcon(ctx, hashOrValue, size, config) { + if (!ctx) { + throw new Error("No canvas specified."); + } + + iconGenerator(new CanvasRenderer(ctx, size), + isValidHash(hashOrValue) || computeHash(hashOrValue), + config); + + var canvas = ctx.canvas; + if (canvas) { + canvas[IS_RENDERED_PROPERTY] = true; + } +} + +/** + * Prepares a measure to be used as a measure in an SVG path, by + * rounding the measure to a single decimal. This reduces the file + * size of the generated SVG with more than 50% in some cases. + */ +function svgValue(value) { + return ((value * 10 + 0.5) | 0) / 10; +} + +/** + * Represents an SVG path element. + */ +function SvgPath() { + /** + * This property holds the data string (path.d) of the SVG path. + * @type {string} + */ + this.B/*dataString*/ = ""; +} +var SvgPath__prototype = SvgPath.prototype; + +/** + * Adds a polygon with the current fill color to the SVG path. + * @param points An array of Point objects. + */ +SvgPath__prototype.g/*addPolygon*/ = function addPolygon (points) { + var dataString = ""; + for (var i = 0; i < points.length; i++) { + dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y); + } + this.B/*dataString*/ += dataString + "Z"; +}; + +/** + * Adds a circle with the current fill color to the SVG path. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +SvgPath__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + var sweepFlag = counterClockwise ? 0 : 1, + svgRadius = svgValue(diameter / 2), + svgDiameter = svgValue(diameter), + svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " "; + + this.B/*dataString*/ += + "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) + + svgArc + svgDiameter + ",0" + + svgArc + (-svgDiameter) + ",0"; +}; + + + +/** + * Renderer producing SVG output. + * @implements {Renderer} + */ +function SvgRenderer(target) { + /** + * @type {SvgPath} + * @private + */ + this.C/*_path*/; + + /** + * @type {Object.<string,SvgPath>} + * @private + */ + this.D/*_pathsByColor*/ = { }; + + /** + * @type {SvgElement|SvgWriter} + * @private + */ + this.R/*_target*/ = target; + + /** + * @type {number} + */ + this.k/*iconSize*/ = target.k/*iconSize*/; +} +var SvgRenderer__prototype = SvgRenderer.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb[aa]. + */ +SvgRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { + var match = /^(#......)(..)?/.exec(fillColor), + opacity = match[2] ? parseHex(match[2], 0) / 255 : 1; + this.R/*_target*/.m/*setBackground*/(match[1], opacity); +}; + +/** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} color Fill color on format #xxxxxx. + */ +SvgRenderer__prototype.O/*beginShape*/ = function beginShape (color) { + this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath()); +}; + +/** + * Marks the end of the currently drawn shape. + */ +SvgRenderer__prototype.P/*endShape*/ = function endShape () { }; + +/** + * Adds a polygon with the current fill color to the SVG. + * @param points An array of Point objects. + */ +SvgRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { + this.C/*_path*/.g/*addPolygon*/(points); +}; + +/** + * Adds a circle with the current fill color to the SVG. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ +SvgRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { + this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise); +}; + +/** + * Called when the icon has been completely drawn. + */ +SvgRenderer__prototype.finish = function finish () { + var this$1 = this; + + var pathsByColor = this.D/*_pathsByColor*/; + for (var color in pathsByColor) { + // hasOwnProperty cannot be shadowed in pathsByColor + // eslint-disable-next-line no-prototype-builtins + if (pathsByColor.hasOwnProperty(color)) { + this$1.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/); + } + } +}; + +var SVG_CONSTANTS = { + T/*XMLNS*/: "http://www.w3.org/2000/svg", + U/*WIDTH*/: "width", + V/*HEIGHT*/: "height", +}; + +/** + * Renderer producing SVG output. + */ +function SvgWriter(iconSize) { + /** + * @type {number} + */ + this.k/*iconSize*/ = iconSize; + + /** + * @type {string} + * @private + */ + this.F/*_s*/ = + '<svg xmlns="' + SVG_CONSTANTS.T/*XMLNS*/ + '" width="' + + iconSize + '" height="' + iconSize + '" viewBox="0 0 ' + + iconSize + ' ' + iconSize + '">'; +} +var SvgWriter__prototype = SvgWriter.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb. + * @param {number} opacity Opacity in the range [0.0, 1.0]. + */ +SvgWriter__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { + if (opacity) { + this.F/*_s*/ += '<rect width="100%" height="100%" fill="' + + fillColor + '" opacity="' + opacity.toFixed(2) + '"/>'; + } +}; + +/** + * Writes a path to the SVG string. + * @param {string} color Fill color on format #rrggbb. + * @param {string} dataString The SVG path data string. + */ +SvgWriter__prototype.S/*appendPath*/ = function appendPath (color, dataString) { + this.F/*_s*/ += '<path fill="' + color + '" d="' + dataString + '"/>'; +}; + +/** + * Gets the rendered image as an SVG string. + */ +SvgWriter__prototype.toString = function toString () { + return this.F/*_s*/ + "</svg>"; +}; + +/** + * Draws an identicon as an SVG string. + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. + * @param {number} size - Icon size in pixels. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + * @returns {string} SVG string + */ +function toSvg(hashOrValue, size, config) { + var writer = new SvgWriter(size); + iconGenerator(new SvgRenderer(writer), + isValidHash(hashOrValue) || computeHash(hashOrValue), + config); + return writer.toString(); +} + +/** + * Creates a new element and adds it to the specified parent. + * @param {Element} parentNode + * @param {string} name + * @param {...(string|number)} keyValuePairs + */ +function SvgElement_append(parentNode, name) { + var keyValuePairs = [], len = arguments.length - 2; + while ( len-- > 0 ) keyValuePairs[ len ] = arguments[ len + 2 ]; + + var el = document.createElementNS(SVG_CONSTANTS.T/*XMLNS*/, name); + + for (var i = 0; i + 1 < keyValuePairs.length; i += 2) { + el.setAttribute( + /** @type {string} */(keyValuePairs[i]), + /** @type {string} */(keyValuePairs[i + 1]) + ); + } + + parentNode.appendChild(el); +} + + +/** + * Renderer producing SVG output. + */ +function SvgElement(element) { + // Don't use the clientWidth and clientHeight properties on SVG elements + // since Firefox won't serve a proper value of these properties on SVG + // elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811) + // Instead use 100px as a hardcoded size (the svg viewBox will rescale + // the icon to the correct dimensions) + var iconSize = this.k/*iconSize*/ = Math.min( + (Number(element.getAttribute(SVG_CONSTANTS.U/*WIDTH*/)) || 100), + (Number(element.getAttribute(SVG_CONSTANTS.V/*HEIGHT*/)) || 100) + ); + + /** + * @type {Element} + * @private + */ + this.W/*_el*/ = element; + + // Clear current SVG child elements + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + // Set viewBox attribute to ensure the svg scales nicely. + element.setAttribute("viewBox", "0 0 " + iconSize + " " + iconSize); + element.setAttribute("preserveAspectRatio", "xMidYMid meet"); +} +var SvgElement__prototype = SvgElement.prototype; + +/** + * Fills the background with the specified color. + * @param {string} fillColor Fill color on the format #rrggbb. + * @param {number} opacity Opacity in the range [0.0, 1.0]. + */ +SvgElement__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { + if (opacity) { + SvgElement_append(this.W/*_el*/, "rect", + SVG_CONSTANTS.U/*WIDTH*/, "100%", + SVG_CONSTANTS.V/*HEIGHT*/, "100%", + "fill", fillColor, + "opacity", opacity); + } +}; + +/** + * Appends a path to the SVG element. + * @param {string} color Fill color on format #xxxxxx. + * @param {string} dataString The SVG path data string. + */ +SvgElement__prototype.S/*appendPath*/ = function appendPath (color, dataString) { + SvgElement_append(this.W/*_el*/, "path", + "fill", color, + "d", dataString); +}; + +/** + * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute. + */ +function updateAll() { + if (documentQuerySelectorAll) { + update(ICON_SELECTOR); + } +} + +/** + * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute that have not already + * been rendered. + */ +function updateAllConditional() { + if (documentQuerySelectorAll) { + /** @type {NodeListOf<HTMLElement>} */ + var elements = documentQuerySelectorAll(ICON_SELECTOR); + + for (var i = 0; i < elements.length; i++) { + var el = elements[i]; + if (!el[IS_RENDERED_PROPERTY]) { + update(el); + } + } + } +} + +/** + * Updates the identicon in the specified `<canvas>` or `<svg>` elements. + * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type + * `<svg>` or `<canvas>`, or a CSS selector to such an element. + * @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or + * `data-jdenticon-value` attribute will be evaluated. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any + * global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function update(el, hashOrValue, config) { + renderDomElement(el, hashOrValue, config, function (el, iconType) { + if (iconType) { + return iconType == ICON_TYPE_SVG ? + new SvgRenderer(new SvgElement(el)) : + new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d")); + } + }); +} + +/** + * Updates the identicon in the specified canvas or svg elements. + * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type + * `<svg>` or `<canvas>`, or a CSS selector to such an element. + * @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or + * `data-jdenticon-value` attribute will be evaluated. + * @param {Object|number|undefined} config + * @param {function(Element,number):Renderer} rendererFactory - Factory function for creating an icon renderer. + */ +function renderDomElement(el, hashOrValue, config, rendererFactory) { + if (typeof el === "string") { + if (documentQuerySelectorAll) { + var elements = documentQuerySelectorAll(el); + for (var i = 0; i < elements.length; i++) { + renderDomElement(elements[i], hashOrValue, config, rendererFactory); + } + } + return; + } + + // Hash selection. The result from getValidHash or computeHash is + // accepted as a valid hash. + var hash = + // 1. Explicit valid hash + isValidHash(hashOrValue) || + + // 2. Explicit value (`!= null` catches both null and undefined) + hashOrValue != null && computeHash(hashOrValue) || + + // 3. `data-jdenticon-hash` attribute + isValidHash(el.getAttribute(ATTRIBUTES.t/*HASH*/)) || + + // 4. `data-jdenticon-value` attribute. + // We want to treat an empty attribute as an empty value. + // Some browsers return empty string even if the attribute + // is not specified, so use hasAttribute to determine if + // the attribute is specified. + el.hasAttribute(ATTRIBUTES.o/*VALUE*/) && computeHash(el.getAttribute(ATTRIBUTES.o/*VALUE*/)); + + if (!hash) { + // No hash specified. Don't render an icon. + return; + } + + var renderer = rendererFactory(el, getIdenticonType(el)); + if (renderer) { + // Draw icon + iconGenerator(renderer, hash, config); + el[IS_RENDERED_PROPERTY] = true; + } +} + +/** + * Renders an identicon for all matching supported elements. + * + * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not + * specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be + * evaluated. + * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global + * configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be + * specified in place of a configuration object. + */ +function jdenticonJqueryPlugin(hashOrValue, config) { + this["each"](function (index, el) { + update(el, hashOrValue, config); + }); + return this; +} + +// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js + +var jdenticon = updateAll; + +defineConfigProperty(jdenticon); + +// Export public API +jdenticon["configure"] = configure; +jdenticon["drawIcon"] = drawIcon; +jdenticon["toSvg"] = toSvg; +jdenticon["update"] = update; +jdenticon["updateCanvas"] = update; +jdenticon["updateSvg"] = update; + +/** + * Specifies the version of the Jdenticon package in use. + * @type {string} + */ +jdenticon["version"] = "3.3.0"; + +/** + * Specifies which bundle of Jdenticon that is used. + * @type {string} + */ +jdenticon["bundle"] = "browser-umd"; + +// Basic jQuery plugin +var jQuery = GLOBAL["jQuery"]; +if (jQuery) { + jQuery["fn"]["jdenticon"] = jdenticonJqueryPlugin; +} + +/** + * This function is called once upon page load. + */ +function jdenticonStartup() { + var replaceMode = ( + jdenticon[CONFIG_PROPERTIES.n/*MODULE*/] || + GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || + { } + )["replaceMode"]; + + if (replaceMode != "never") { + updateAllConditional(); + + if (replaceMode == "observe") { + observer(update); + } + } +} + +// Schedule to render all identicons on the page once it has been loaded. +whenDocumentIsReady(jdenticonStartup); + +return jdenticon; + +}); +\ No newline at end of file diff --git a/src/static/scripts/jdenticon.js b/src/static/scripts/jdenticon.js @@ -1,1462 +0,0 @@ -/** - * Jdenticon 3.2.0 - * http://jdenticon.com - * - * Built: 2022-08-07T11:23:11.640Z - * - * MIT License - * - * Copyright (c) 2014-2021 Daniel Mester Pirttijärvi - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -(function (umdGlobal, factory) { - var jdenticon = factory(umdGlobal); - - // Node.js - if (typeof module !== "undefined" && "exports" in module) { - module["exports"] = jdenticon; - } - // RequireJS - else if (typeof define === "function" && define["amd"]) { - define([], function () { return jdenticon; }); - } - // No module loader - else { - umdGlobal["jdenticon"] = jdenticon; - } -})(typeof self !== "undefined" ? self : this, function (umdGlobal) { -'use strict'; - -/** - * Parses a substring of the hash as a number. - * @param {number} startPosition - * @param {number=} octets - */ -function parseHex(hash, startPosition, octets) { - return parseInt(hash.substr(startPosition, octets), 16); -} - -function decToHex(v) { - v |= 0; // Ensure integer value - return v < 0 ? "00" : - v < 16 ? "0" + v.toString(16) : - v < 256 ? v.toString(16) : - "ff"; -} - -function hueToRgb(m1, m2, h) { - h = h < 0 ? h + 6 : h > 6 ? h - 6 : h; - return decToHex(255 * ( - h < 1 ? m1 + (m2 - m1) * h : - h < 3 ? m2 : - h < 4 ? m1 + (m2 - m1) * (4 - h) : - m1)); -} - -/** - * @param {string} color Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported. - * @returns {string} - */ -function parseColor(color) { - if (/^#[0-9a-f]{3,8}$/i.test(color)) { - var result; - var colorLength = color.length; - - if (colorLength < 6) { - var r = color[1], - g = color[2], - b = color[3], - a = color[4] || ""; - result = "#" + r + r + g + g + b + b + a + a; - } - if (colorLength == 7 || colorLength > 8) { - result = color; - } - - return result; - } -} - -/** - * Converts a hexadecimal color to a CSS3 compatible color. - * @param {string} hexColor Color on the format "#RRGGBB" or "#RRGGBBAA" - * @returns {string} - */ -function toCss3Color(hexColor) { - var a = parseHex(hexColor, 7, 2); - var result; - - if (isNaN(a)) { - result = hexColor; - } else { - var r = parseHex(hexColor, 1, 2), - g = parseHex(hexColor, 3, 2), - b = parseHex(hexColor, 5, 2); - result = "rgba(" + r + "," + g + "," + b + "," + (a / 255).toFixed(2) + ")"; - } - - return result; -} - -/** - * Converts an HSL color to a hexadecimal RGB color. - * @param {number} hue Hue in range [0, 1] - * @param {number} saturation Saturation in range [0, 1] - * @param {number} lightness Lightness in range [0, 1] - * @returns {string} - */ -function hsl(hue, saturation, lightness) { - // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color - var result; - - if (saturation == 0) { - var partialHex = decToHex(lightness * 255); - result = partialHex + partialHex + partialHex; - } - else { - var m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation, - m1 = lightness * 2 - m2; - result = - hueToRgb(m1, m2, hue * 6 + 2) + - hueToRgb(m1, m2, hue * 6) + - hueToRgb(m1, m2, hue * 6 - 2); - } - - return "#" + result; -} - -/** - * Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues - * @param {number} hue Hue in range [0, 1] - * @param {number} saturation Saturation in range [0, 1] - * @param {number} lightness Lightness in range [0, 1] - * @returns {string} - */ -function correctedHsl(hue, saturation, lightness) { - // The corrector specifies the perceived middle lightness for each hue - var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], - corrector = correctors[(hue * 6 + 0.5) | 0]; - - // Adjust the input lightness relative to the corrector - lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2; - - return hsl(hue, saturation, lightness); -} - -/* global umdGlobal */ - -// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for -// backward compatibility. -var GLOBAL = umdGlobal; - -/** - * @typedef {Object} ParsedConfiguration - * @property {number} colorSaturation - * @property {number} grayscaleSaturation - * @property {string} backColor - * @property {number} iconPadding - * @property {function(number):number} hue - * @property {function(number):number} colorLightness - * @property {function(number):number} grayscaleLightness - */ - -var CONFIG_PROPERTIES = { - G/*GLOBAL*/: "jdenticon_config", - n/*MODULE*/: "config", -}; - -var rootConfigurationHolder = {}; - -/** - * Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console - * when it is being used. - * @param {!Object} rootObject - */ -function defineConfigProperty(rootObject) { - rootConfigurationHolder = rootObject; -} - -/** - * Sets a new icon style configuration. The new configuration is not merged with the previous one. * - * @param {Object} newConfiguration - New configuration object. - */ -function configure(newConfiguration) { - if (arguments.length) { - rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] = newConfiguration; - } - return rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/]; -} - -/** - * Gets the normalized current Jdenticon color configuration. Missing fields have default values. - * @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A - * local configuration overrides the global configuration in it entirety. This parameter can for backward - * compatibility also contain a padding value. A padding value only overrides the global padding, not the - * entire global configuration. - * @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor - * explicitly to the API method. - * @returns {ParsedConfiguration} - */ -function getConfiguration(paddingOrLocalConfig, defaultPadding) { - var configObject = - typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig || - rootConfigurationHolder[CONFIG_PROPERTIES.n/*MODULE*/] || - GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || - { }, - - lightnessConfig = configObject["lightness"] || { }, - - // In versions < 2.1.0 there was no grayscale saturation - - // saturation was the color saturation. - saturation = configObject["saturation"] || { }, - colorSaturation = "color" in saturation ? saturation["color"] : saturation, - grayscaleSaturation = saturation["grayscale"], - - backColor = configObject["backColor"], - padding = configObject["padding"]; - - /** - * Creates a lightness range. - */ - function lightness(configName, defaultRange) { - var range = lightnessConfig[configName]; - - // Check if the lightness range is an array-like object. This way we ensure the - // array contain two values at the same time. - if (!(range && range.length > 1)) { - range = defaultRange; - } - - /** - * Gets a lightness relative the specified value in the specified lightness range. - */ - return function (value) { - value = range[0] + value * (range[1] - range[0]); - return value < 0 ? 0 : value > 1 ? 1 : value; - }; - } - - /** - * Gets a hue allowed by the configured hue restriction, - * provided the originally computed hue. - */ - function hueFunction(originalHue) { - var hueConfig = configObject["hues"]; - var hue; - - // Check if 'hues' is an array-like object. This way we also ensure that - // the array is not empty, which would mean no hue restriction. - if (hueConfig && hueConfig.length > 0) { - // originalHue is in the range [0, 1] - // Multiply with 0.999 to change the range to [0, 1) and then truncate the index. - hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)]; - } - - return typeof hue == "number" ? - - // A hue was specified. We need to convert the hue from - // degrees on any turn - e.g. 746° is a perfectly valid hue - - // to turns in the range [0, 1). - ((((hue / 360) % 1) + 1) % 1) : - - // No hue configured => use original hue - originalHue; - } - - return { - X/*hue*/: hueFunction, - p/*colorSaturation*/: typeof colorSaturation == "number" ? colorSaturation : 0.5, - H/*grayscaleSaturation*/: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0, - q/*colorLightness*/: lightness("color", [0.4, 0.8]), - I/*grayscaleLightness*/: lightness("grayscale", [0.3, 0.9]), - J/*backColor*/: parseColor(backColor), - Y/*iconPadding*/: - typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig : - typeof padding == "number" ? padding : - defaultPadding - } -} - -var ICON_TYPE_SVG = 1; - -var ICON_TYPE_CANVAS = 2; - -var ATTRIBUTES = { - t/*HASH*/: "data-jdenticon-hash", - o/*VALUE*/: "data-jdenticon-value" -}; - -var ICON_SELECTOR = "[" + ATTRIBUTES.t/*HASH*/ +"],[" + ATTRIBUTES.o/*VALUE*/ +"]"; - -var documentQuerySelectorAll = /** @type {!Function} */ ( - typeof document !== "undefined" && document.querySelectorAll.bind(document)); - -function getIdenticonType(el) { - if (el) { - var tagName = el["tagName"]; - - if (/^svg$/i.test(tagName)) { - return ICON_TYPE_SVG; - } - - if (/^canvas$/i.test(tagName) && "getContext" in el) { - return ICON_TYPE_CANVAS; - } - } -} - -function observer(updateCallback) { - if (typeof MutationObserver != "undefined") { - var mutationObserver = new MutationObserver(function onmutation(mutations) { - for (var mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) { - var mutation = mutations[mutationIndex]; - var addedNodes = mutation.addedNodes; - - for (var addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) { - var addedNode = addedNodes[addedNodeIndex]; - - // Skip other types of nodes than element nodes, since they might not support - // the querySelectorAll method => runtime error. - if (addedNode.nodeType == 1) { - if (getIdenticonType(addedNode)) { - updateCallback(addedNode); - } - else { - var icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR); - for (var iconIndex = 0; iconIndex < icons.length; iconIndex++) { - updateCallback(icons[iconIndex]); - } - } - } - } - - if (mutation.type == "attributes" && getIdenticonType(mutation.target)) { - updateCallback(mutation.target); - } - } - }); - - mutationObserver.observe(document.body, { - "childList": true, - "attributes": true, - "attributeFilter": [ATTRIBUTES.o/*VALUE*/, ATTRIBUTES.t/*HASH*/, "width", "height"], - "subtree": true, - }); - } -} - -/** - * Represents a point. - */ -function Point(x, y) { - this.x = x; - this.y = y; -} - -/** - * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, - * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. - */ -function Transform(x, y, size, rotation) { - this.u/*_x*/ = x; - this.v/*_y*/ = y; - this.K/*_size*/ = size; - this.Z/*_rotation*/ = rotation; -} - -/** - * Transforms the specified point based on the translation and rotation specification for this Transform. - * @param {number} x x-coordinate - * @param {number} y y-coordinate - * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. - * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. - */ -Transform.prototype.L/*transformIconPoint*/ = function transformIconPoint (x, y, w, h) { - var right = this.u/*_x*/ + this.K/*_size*/, - bottom = this.v/*_y*/ + this.K/*_size*/, - rotation = this.Z/*_rotation*/; - return rotation === 1 ? new Point(right - y - (h || 0), this.v/*_y*/ + x) : - rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : - rotation === 3 ? new Point(this.u/*_x*/ + y, bottom - x - (w || 0)) : - new Point(this.u/*_x*/ + x, this.v/*_y*/ + y); -}; - -var NO_TRANSFORM = new Transform(0, 0, 0, 0); - - - -/** - * Provides helper functions for rendering common basic shapes. - */ -function Graphics(renderer) { - /** - * @type {Renderer} - * @private - */ - this.M/*_renderer*/ = renderer; - - /** - * @type {Transform} - */ - this.A/*currentTransform*/ = NO_TRANSFORM; -} -var Graphics__prototype = Graphics.prototype; - -/** - * Adds a polygon to the underlying renderer. - * @param {Array<number>} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] - * @param {boolean=} invert Specifies if the polygon will be inverted. - */ -Graphics__prototype.g/*addPolygon*/ = function addPolygon (points, invert) { - var this$1 = this; - - var di = invert ? -2 : 2, - transformedPoints = []; - - for (var i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { - transformedPoints.push(this$1.A/*currentTransform*/.L/*transformIconPoint*/(points[i], points[i + 1])); - } - - this.M/*_renderer*/.g/*addPolygon*/(transformedPoints); -}; - -/** - * Adds a polygon to the underlying renderer. - * Source: http://stackoverflow.com/a/2173084 - * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. - * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. - * @param {number} size The size of the ellipse. - * @param {boolean=} invert Specifies if the ellipse will be inverted. - */ -Graphics__prototype.h/*addCircle*/ = function addCircle (x, y, size, invert) { - var p = this.A/*currentTransform*/.L/*transformIconPoint*/(x, y, size, size); - this.M/*_renderer*/.h/*addCircle*/(p, size, invert); -}; - -/** - * Adds a rectangle to the underlying renderer. - * @param {number} x The x-coordinate of the upper left corner of the rectangle. - * @param {number} y The y-coordinate of the upper left corner of the rectangle. - * @param {number} w The width of the rectangle. - * @param {number} h The height of the rectangle. - * @param {boolean=} invert Specifies if the rectangle will be inverted. - */ -Graphics__prototype.i/*addRectangle*/ = function addRectangle (x, y, w, h, invert) { - this.g/*addPolygon*/([ - x, y, - x + w, y, - x + w, y + h, - x, y + h - ], invert); -}; - -/** - * Adds a right triangle to the underlying renderer. - * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. - * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. - * @param {number} w The width of the triangle. - * @param {number} h The height of the triangle. - * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. - * @param {boolean=} invert Specifies if the triangle will be inverted. - */ -Graphics__prototype.j/*addTriangle*/ = function addTriangle (x, y, w, h, r, invert) { - var points = [ - x + w, y, - x + w, y + h, - x, y + h, - x, y - ]; - points.splice(((r || 0) % 4) * 2, 2); - this.g/*addPolygon*/(points, invert); -}; - -/** - * Adds a rhombus to the underlying renderer. - * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. - * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. - * @param {number} w The width of the rhombus. - * @param {number} h The height of the rhombus. - * @param {boolean=} invert Specifies if the rhombus will be inverted. - */ -Graphics__prototype.N/*addRhombus*/ = function addRhombus (x, y, w, h, invert) { - this.g/*addPolygon*/([ - x + w / 2, y, - x + w, y + h / 2, - x + w / 2, y + h, - x, y + h / 2 - ], invert); -}; - -/** - * @param {number} index - * @param {Graphics} g - * @param {number} cell - * @param {number} positionIndex - */ -function centerShape(index, g, cell, positionIndex) { - index = index % 14; - - var k, m, w, h, inner, outer; - - !index ? ( - k = cell * 0.42, - g.g/*addPolygon*/([ - 0, 0, - cell, 0, - cell, cell - k * 2, - cell - k, cell, - 0, cell - ])) : - - index == 1 ? ( - w = 0 | (cell * 0.5), - h = 0 | (cell * 0.8), - - g.j/*addTriangle*/(cell - w, 0, w, h, 2)) : - - index == 2 ? ( - w = 0 | (cell / 3), - g.i/*addRectangle*/(w, w, cell - w, cell - w)) : - - index == 3 ? ( - inner = cell * 0.1, - // Use fixed outer border widths in small icons to ensure the border is drawn - outer = - cell < 6 ? 1 : - cell < 8 ? 2 : - (0 | (cell * 0.25)), - - inner = - inner > 1 ? (0 | inner) : // large icon => truncate decimals - inner > 0.5 ? 1 : // medium size icon => fixed width - inner, // small icon => anti-aliased border - - g.i/*addRectangle*/(outer, outer, cell - inner - outer, cell - inner - outer)) : - - index == 4 ? ( - m = 0 | (cell * 0.15), - w = 0 | (cell * 0.5), - g.h/*addCircle*/(cell - w - m, cell - w - m, w)) : - - index == 5 ? ( - inner = cell * 0.1, - outer = inner * 4, - - // Align edge to nearest pixel in large icons - outer > 3 && (outer = 0 | outer), - - g.i/*addRectangle*/(0, 0, cell, cell), - g.g/*addPolygon*/([ - outer, outer, - cell - inner, outer, - outer + (cell - outer - inner) / 2, cell - inner - ], true)) : - - index == 6 ? - g.g/*addPolygon*/([ - 0, 0, - cell, 0, - cell, cell * 0.7, - cell * 0.4, cell * 0.4, - cell * 0.7, cell, - 0, cell - ]) : - - index == 7 ? - g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : - - index == 8 ? ( - g.i/*addRectangle*/(0, 0, cell, cell / 2), - g.i/*addRectangle*/(0, cell / 2, cell / 2, cell / 2), - g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 1)) : - - index == 9 ? ( - inner = cell * 0.14, - // Use fixed outer border widths in small icons to ensure the border is drawn - outer = - cell < 4 ? 1 : - cell < 6 ? 2 : - (0 | (cell * 0.35)), - - inner = - cell < 8 ? inner : // small icon => anti-aliased border - (0 | inner), // large icon => truncate decimals - - g.i/*addRectangle*/(0, 0, cell, cell), - g.i/*addRectangle*/(outer, outer, cell - outer - inner, cell - outer - inner, true)) : - - index == 10 ? ( - inner = cell * 0.12, - outer = inner * 3, - - g.i/*addRectangle*/(0, 0, cell, cell), - g.h/*addCircle*/(outer, outer, cell - inner - outer, true)) : - - index == 11 ? - g.j/*addTriangle*/(cell / 2, cell / 2, cell / 2, cell / 2, 3) : - - index == 12 ? ( - m = cell * 0.25, - g.i/*addRectangle*/(0, 0, cell, cell), - g.N/*addRhombus*/(m, m, cell - m, cell - m, true)) : - - // 13 - ( - !positionIndex && ( - m = cell * 0.4, w = cell * 1.2, - g.h/*addCircle*/(m, m, w) - ) - ); -} - -/** - * @param {number} index - * @param {Graphics} g - * @param {number} cell - */ -function outerShape(index, g, cell) { - index = index % 4; - - var m; - - !index ? - g.j/*addTriangle*/(0, 0, cell, cell, 0) : - - index == 1 ? - g.j/*addTriangle*/(0, cell / 2, cell, cell / 2, 0) : - - index == 2 ? - g.N/*addRhombus*/(0, 0, cell, cell) : - - // 3 - ( - m = cell / 6, - g.h/*addCircle*/(m, m, cell - 2 * m) - ); -} - -/** - * Gets a set of identicon color candidates for a specified hue and config. - * @param {number} hue - * @param {ParsedConfiguration} config - */ -function colorTheme(hue, config) { - hue = config.X/*hue*/(hue); - return [ - // Dark gray - correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(0)), - // Mid color - correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0.5)), - // Light gray - correctedHsl(hue, config.H/*grayscaleSaturation*/, config.I/*grayscaleLightness*/(1)), - // Light color - correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(1)), - // Dark color - correctedHsl(hue, config.p/*colorSaturation*/, config.q/*colorLightness*/(0)) - ]; -} - -/** - * Draws an identicon to a specified renderer. - * @param {Renderer} renderer - * @param {string} hash - * @param {Object|number=} config - */ -function iconGenerator(renderer, hash, config) { - var parsedConfig = getConfiguration(config, 0.08); - - // Set background color - if (parsedConfig.J/*backColor*/) { - renderer.m/*setBackground*/(parsedConfig.J/*backColor*/); - } - - // Calculate padding and round to nearest integer - var size = renderer.k/*iconSize*/; - var padding = (0.5 + size * parsedConfig.Y/*iconPadding*/) | 0; - size -= padding * 2; - - var graphics = new Graphics(renderer); - - // Calculate cell size and ensure it is an integer - var cell = 0 | (size / 4); - - // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon - var x = 0 | (padding + size / 2 - cell * 2); - var y = 0 | (padding + size / 2 - cell * 2); - - function renderShape(colorIndex, shapes, index, rotationIndex, positions) { - var shapeIndex = parseHex(hash, index, 1); - var r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0; - - renderer.O/*beginShape*/(availableColors[selectedColorIndexes[colorIndex]]); - - for (var i = 0; i < positions.length; i++) { - graphics.A/*currentTransform*/ = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4); - shapes(shapeIndex, graphics, cell, i); - } - - renderer.P/*endShape*/(); - } - - // AVAILABLE COLORS - var hue = parseHex(hash, -7) / 0xfffffff, - - // Available colors for this icon - availableColors = colorTheme(hue, parsedConfig), - - // The index of the selected colors - selectedColorIndexes = []; - - var index; - - function isDuplicate(values) { - if (values.indexOf(index) >= 0) { - for (var i = 0; i < values.length; i++) { - if (selectedColorIndexes.indexOf(values[i]) >= 0) { - return true; - } - } - } - } - - for (var i = 0; i < 3; i++) { - index = parseHex(hash, 8 + i, 1) % availableColors.length; - if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo - isDuplicate([2, 3])) { // Disallow light gray and light color combo - index = 1; - } - selectedColorIndexes.push(index); - } - - // ACTUAL RENDERING - // Sides - renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); - // Corners - renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); - // Center - renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); - - renderer.finish(); -} - -/** - * Computes a SHA1 hash for any value and returns it as a hexadecimal string. - * - * This function is optimized for minimal code size and rather short messages. - * - * @param {string} message - */ -function sha1(message) { - var HASH_SIZE_HALF_BYTES = 40; - var BLOCK_SIZE_WORDS = 16; - - // Variables - // `var` is used to be able to minimize the number of `var` keywords. - var i = 0, - f = 0, - - // Use `encodeURI` to UTF8 encode the message without any additional libraries - // We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky - // since `unescape` is deprecated. - urlEncodedMessage = encodeURI(message) + "%80", // trailing '1' bit padding - - // This can be changed to a preallocated Uint32Array array for greater performance and larger code size - data = [], - dataSize, - - hashBuffer = [], - - a = 0x67452301, - b = 0xefcdab89, - c = ~a, - d = ~b, - e = 0xc3d2e1f0, - hash = [a, b, c, d, e], - - blockStartIndex = 0, - hexHash = ""; - - /** - * Rotates the value a specified number of bits to the left. - * @param {number} value Value to rotate - * @param {number} shift Bit count to shift. - */ - function rotl(value, shift) { - return (value << shift) | (value >>> (32 - shift)); - } - - // Message data - for ( ; i < urlEncodedMessage.length; f++) { - data[f >> 2] = data[f >> 2] | - ( - ( - urlEncodedMessage[i] == "%" - // Percent encoded byte - ? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16) - // Unencoded byte - : urlEncodedMessage.charCodeAt(i++) - ) - - // Read bytes in reverse order (big endian words) - << ((3 - (f & 3)) * 8) - ); - } - - // f is now the length of the utf8 encoded message - // 7 = 8 bytes (64 bit) for message size, -1 to round down - // >> 6 = integer division with block size - dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS; - - // Message size in bits. - // SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least - // significant 32 bits are set. -8 is for the '1' bit padding byte. - data[dataSize - 1] = f * 8 - 8; - - // Compute hash - for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) { - for (i = 0; i < 80; i++) { - f = rotl(a, 5) + e + ( - // Ch - i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 : - - // Parity - i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 : - - // Maj - i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc : - - // Parity - (b ^ c ^ d) + 0xca62c1d6 - ) + ( - hashBuffer[i] = i < BLOCK_SIZE_WORDS - // Bitwise OR is used to coerse `undefined` to 0 - ? (data[blockStartIndex + i] | 0) - : rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1) - ); - - e = d; - d = c; - c = rotl(b, 30); - b = a; - a = f; - } - - hash[0] = a = ((hash[0] + a) | 0); - hash[1] = b = ((hash[1] + b) | 0); - hash[2] = c = ((hash[2] + c) | 0); - hash[3] = d = ((hash[3] + d) | 0); - hash[4] = e = ((hash[4] + e) | 0); - } - - // Format hex hash - for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) { - hexHash += ( - ( - // Get word (2^3 half-bytes per word) - hash[i >> 3] >>> - - // Append half-bytes in reverse order - ((7 - (i & 7)) * 4) - ) - // Clamp to half-byte - & 0xf - ).toString(16); - } - - return hexHash; -} - -/** - * Inputs a value that might be a valid hash string for Jdenticon and returns it - * if it is determined valid, otherwise a falsy value is returned. - */ -function isValidHash(hashCandidate) { - return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate; -} - -/** - * Computes a hash for the specified value. Currently SHA1 is used. This function - * always returns a valid hash. - */ -function computeHash(value) { - return sha1(value == null ? "" : "" + value); -} - - - -/** - * Renderer redirecting drawing commands to a canvas context. - * @implements {Renderer} - */ -function CanvasRenderer(ctx, iconSize) { - var canvas = ctx.canvas; - var width = canvas.width; - var height = canvas.height; - - ctx.save(); - - if (!iconSize) { - iconSize = Math.min(width, height); - - ctx.translate( - ((width - iconSize) / 2) | 0, - ((height - iconSize) / 2) | 0); - } - - /** - * @private - */ - this.l/*_ctx*/ = ctx; - this.k/*iconSize*/ = iconSize; - - ctx.clearRect(0, 0, iconSize, iconSize); -} -var CanvasRenderer__prototype = CanvasRenderer.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb[aa]. - */ -CanvasRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { - var ctx = this.l/*_ctx*/; - var iconSize = this.k/*iconSize*/; - - ctx.fillStyle = toCss3Color(fillColor); - ctx.fillRect(0, 0, iconSize, iconSize); -}; - -/** - * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. - * @param {string} fillColor Fill color on format #rrggbb[aa]. - */ -CanvasRenderer__prototype.O/*beginShape*/ = function beginShape (fillColor) { - var ctx = this.l/*_ctx*/; - ctx.fillStyle = toCss3Color(fillColor); - ctx.beginPath(); -}; - -/** - * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas. - */ -CanvasRenderer__prototype.P/*endShape*/ = function endShape () { - this.l/*_ctx*/.fill(); -}; - -/** - * Adds a polygon to the rendering queue. - * @param points An array of Point objects. - */ -CanvasRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { - var ctx = this.l/*_ctx*/; - ctx.moveTo(points[0].x, points[0].y); - for (var i = 1; i < points.length; i++) { - ctx.lineTo(points[i].x, points[i].y); - } - ctx.closePath(); -}; - -/** - * Adds a circle to the rendering queue. - * @param {Point} point The upper left corner of the circle bounding box. - * @param {number} diameter The diameter of the circle. - * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). - */ -CanvasRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { - var ctx = this.l/*_ctx*/, - radius = diameter / 2; - ctx.moveTo(point.x + radius, point.y + radius); - ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise); - ctx.closePath(); -}; - -/** - * Called when the icon has been completely drawn. - */ -CanvasRenderer__prototype.finish = function finish () { - this.l/*_ctx*/.restore(); -}; - -/** - * Draws an identicon to a context. - * @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0). - * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. - * @param {number} size - Icon size in pixels. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any - * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - */ -function drawIcon(ctx, hashOrValue, size, config) { - if (!ctx) { - throw new Error("No canvas specified."); - } - - iconGenerator(new CanvasRenderer(ctx, size), - isValidHash(hashOrValue) || computeHash(hashOrValue), - config); -} - -/** - * Prepares a measure to be used as a measure in an SVG path, by - * rounding the measure to a single decimal. This reduces the file - * size of the generated SVG with more than 50% in some cases. - */ -function svgValue(value) { - return ((value * 10 + 0.5) | 0) / 10; -} - -/** - * Represents an SVG path element. - */ -function SvgPath() { - /** - * This property holds the data string (path.d) of the SVG path. - * @type {string} - */ - this.B/*dataString*/ = ""; -} -var SvgPath__prototype = SvgPath.prototype; - -/** - * Adds a polygon with the current fill color to the SVG path. - * @param points An array of Point objects. - */ -SvgPath__prototype.g/*addPolygon*/ = function addPolygon (points) { - var dataString = ""; - for (var i = 0; i < points.length; i++) { - dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y); - } - this.B/*dataString*/ += dataString + "Z"; -}; - -/** - * Adds a circle with the current fill color to the SVG path. - * @param {Point} point The upper left corner of the circle bounding box. - * @param {number} diameter The diameter of the circle. - * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). - */ -SvgPath__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { - var sweepFlag = counterClockwise ? 0 : 1, - svgRadius = svgValue(diameter / 2), - svgDiameter = svgValue(diameter), - svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " "; - - this.B/*dataString*/ += - "M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) + - svgArc + svgDiameter + ",0" + - svgArc + (-svgDiameter) + ",0"; -}; - - - -/** - * Renderer producing SVG output. - * @implements {Renderer} - */ -function SvgRenderer(target) { - /** - * @type {SvgPath} - * @private - */ - this.C/*_path*/; - - /** - * @type {Object.<string,SvgPath>} - * @private - */ - this.D/*_pathsByColor*/ = { }; - - /** - * @type {SvgElement|SvgWriter} - * @private - */ - this.R/*_target*/ = target; - - /** - * @type {number} - */ - this.k/*iconSize*/ = target.k/*iconSize*/; -} -var SvgRenderer__prototype = SvgRenderer.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb[aa]. - */ -SvgRenderer__prototype.m/*setBackground*/ = function setBackground (fillColor) { - var match = /^(#......)(..)?/.exec(fillColor), - opacity = match[2] ? parseHex(match[2], 0) / 255 : 1; - this.R/*_target*/.m/*setBackground*/(match[1], opacity); -}; - -/** - * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. - * @param {string} color Fill color on format #xxxxxx. - */ -SvgRenderer__prototype.O/*beginShape*/ = function beginShape (color) { - this.C/*_path*/ = this.D/*_pathsByColor*/[color] || (this.D/*_pathsByColor*/[color] = new SvgPath()); -}; - -/** - * Marks the end of the currently drawn shape. - */ -SvgRenderer__prototype.P/*endShape*/ = function endShape () { }; - -/** - * Adds a polygon with the current fill color to the SVG. - * @param points An array of Point objects. - */ -SvgRenderer__prototype.g/*addPolygon*/ = function addPolygon (points) { - this.C/*_path*/.g/*addPolygon*/(points); -}; - -/** - * Adds a circle with the current fill color to the SVG. - * @param {Point} point The upper left corner of the circle bounding box. - * @param {number} diameter The diameter of the circle. - * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). - */ -SvgRenderer__prototype.h/*addCircle*/ = function addCircle (point, diameter, counterClockwise) { - this.C/*_path*/.h/*addCircle*/(point, diameter, counterClockwise); -}; - -/** - * Called when the icon has been completely drawn. - */ -SvgRenderer__prototype.finish = function finish () { - var this$1 = this; - - var pathsByColor = this.D/*_pathsByColor*/; - for (var color in pathsByColor) { - // hasOwnProperty cannot be shadowed in pathsByColor - // eslint-disable-next-line no-prototype-builtins - if (pathsByColor.hasOwnProperty(color)) { - this$1.R/*_target*/.S/*appendPath*/(color, pathsByColor[color].B/*dataString*/); - } - } -}; - -var SVG_CONSTANTS = { - T/*XMLNS*/: "http://www.w3.org/2000/svg", - U/*WIDTH*/: "width", - V/*HEIGHT*/: "height", -}; - -/** - * Renderer producing SVG output. - */ -function SvgWriter(iconSize) { - /** - * @type {number} - */ - this.k/*iconSize*/ = iconSize; - - /** - * @type {string} - * @private - */ - this.F/*_s*/ = - '<svg xmlns="' + SVG_CONSTANTS.T/*XMLNS*/ + '" width="' + - iconSize + '" height="' + iconSize + '" viewBox="0 0 ' + - iconSize + ' ' + iconSize + '">'; -} -var SvgWriter__prototype = SvgWriter.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb. - * @param {number} opacity Opacity in the range [0.0, 1.0]. - */ -SvgWriter__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { - if (opacity) { - this.F/*_s*/ += '<rect width="100%" height="100%" fill="' + - fillColor + '" opacity="' + opacity.toFixed(2) + '"/>'; - } -}; - -/** - * Writes a path to the SVG string. - * @param {string} color Fill color on format #rrggbb. - * @param {string} dataString The SVG path data string. - */ -SvgWriter__prototype.S/*appendPath*/ = function appendPath (color, dataString) { - this.F/*_s*/ += '<path fill="' + color + '" d="' + dataString + '"/>'; -}; - -/** - * Gets the rendered image as an SVG string. - */ -SvgWriter__prototype.toString = function toString () { - return this.F/*_s*/ + "</svg>"; -}; - -/** - * Draws an identicon as an SVG string. - * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. - * @param {number} size - Icon size in pixels. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any - * global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - * @returns {string} SVG string - */ -function toSvg(hashOrValue, size, config) { - var writer = new SvgWriter(size); - iconGenerator(new SvgRenderer(writer), - isValidHash(hashOrValue) || computeHash(hashOrValue), - config); - return writer.toString(); -} - -/** - * Creates a new element and adds it to the specified parent. - * @param {Element} parentNode - * @param {string} name - * @param {...(string|number)} keyValuePairs - */ -function SvgElement_append(parentNode, name) { - var keyValuePairs = [], len = arguments.length - 2; - while ( len-- > 0 ) keyValuePairs[ len ] = arguments[ len + 2 ]; - - var el = document.createElementNS(SVG_CONSTANTS.T/*XMLNS*/, name); - - for (var i = 0; i + 1 < keyValuePairs.length; i += 2) { - el.setAttribute( - /** @type {string} */(keyValuePairs[i]), - /** @type {string} */(keyValuePairs[i + 1]) - ); - } - - parentNode.appendChild(el); -} - - -/** - * Renderer producing SVG output. - */ -function SvgElement(element) { - // Don't use the clientWidth and clientHeight properties on SVG elements - // since Firefox won't serve a proper value of these properties on SVG - // elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811) - // Instead use 100px as a hardcoded size (the svg viewBox will rescale - // the icon to the correct dimensions) - var iconSize = this.k/*iconSize*/ = Math.min( - (Number(element.getAttribute(SVG_CONSTANTS.U/*WIDTH*/)) || 100), - (Number(element.getAttribute(SVG_CONSTANTS.V/*HEIGHT*/)) || 100) - ); - - /** - * @type {Element} - * @private - */ - this.W/*_el*/ = element; - - // Clear current SVG child elements - while (element.firstChild) { - element.removeChild(element.firstChild); - } - - // Set viewBox attribute to ensure the svg scales nicely. - element.setAttribute("viewBox", "0 0 " + iconSize + " " + iconSize); - element.setAttribute("preserveAspectRatio", "xMidYMid meet"); -} -var SvgElement__prototype = SvgElement.prototype; - -/** - * Fills the background with the specified color. - * @param {string} fillColor Fill color on the format #rrggbb. - * @param {number} opacity Opacity in the range [0.0, 1.0]. - */ -SvgElement__prototype.m/*setBackground*/ = function setBackground (fillColor, opacity) { - if (opacity) { - SvgElement_append(this.W/*_el*/, "rect", - SVG_CONSTANTS.U/*WIDTH*/, "100%", - SVG_CONSTANTS.V/*HEIGHT*/, "100%", - "fill", fillColor, - "opacity", opacity); - } -}; - -/** - * Appends a path to the SVG element. - * @param {string} color Fill color on format #xxxxxx. - * @param {string} dataString The SVG path data string. - */ -SvgElement__prototype.S/*appendPath*/ = function appendPath (color, dataString) { - SvgElement_append(this.W/*_el*/, "path", - "fill", color, - "d", dataString); -}; - -/** - * Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute. - */ -function updateAll() { - if (documentQuerySelectorAll) { - update(ICON_SELECTOR); - } -} - -/** - * Updates the identicon in the specified `<canvas>` or `<svg>` elements. - * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type - * `<svg>` or `<canvas>`, or a CSS selector to such an element. - * @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or - * `data-jdenticon-value` attribute will be evaluated. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any - * global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - */ -function update(el, hashOrValue, config) { - renderDomElement(el, hashOrValue, config, function (el, iconType) { - if (iconType) { - return iconType == ICON_TYPE_SVG ? - new SvgRenderer(new SvgElement(el)) : - new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d")); - } - }); -} - -/** - * Updates the identicon in the specified canvas or svg elements. - * @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type - * `<svg>` or `<canvas>`, or a CSS selector to such an element. - * @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or - * `data-jdenticon-value` attribute will be evaluated. - * @param {Object|number|undefined} config - * @param {function(Element,number):Renderer} rendererFactory - Factory function for creating an icon renderer. - */ -function renderDomElement(el, hashOrValue, config, rendererFactory) { - if (typeof el === "string") { - if (documentQuerySelectorAll) { - var elements = documentQuerySelectorAll(el); - for (var i = 0; i < elements.length; i++) { - renderDomElement(elements[i], hashOrValue, config, rendererFactory); - } - } - return; - } - - // Hash selection. The result from getValidHash or computeHash is - // accepted as a valid hash. - var hash = - // 1. Explicit valid hash - isValidHash(hashOrValue) || - - // 2. Explicit value (`!= null` catches both null and undefined) - hashOrValue != null && computeHash(hashOrValue) || - - // 3. `data-jdenticon-hash` attribute - isValidHash(el.getAttribute(ATTRIBUTES.t/*HASH*/)) || - - // 4. `data-jdenticon-value` attribute. - // We want to treat an empty attribute as an empty value. - // Some browsers return empty string even if the attribute - // is not specified, so use hasAttribute to determine if - // the attribute is specified. - el.hasAttribute(ATTRIBUTES.o/*VALUE*/) && computeHash(el.getAttribute(ATTRIBUTES.o/*VALUE*/)); - - if (!hash) { - // No hash specified. Don't render an icon. - return; - } - - var renderer = rendererFactory(el, getIdenticonType(el)); - if (renderer) { - // Draw icon - iconGenerator(renderer, hash, config); - } -} - -/** - * Renders an identicon for all matching supported elements. - * - * @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not - * specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be - * evaluated. - * @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global - * configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be - * specified in place of a configuration object. - */ -function jdenticonJqueryPlugin(hashOrValue, config) { - this["each"](function (index, el) { - update(el, hashOrValue, config); - }); - return this; -} - -// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js - -var jdenticon = updateAll; - -defineConfigProperty(jdenticon); - -// Export public API -jdenticon["configure"] = configure; -jdenticon["drawIcon"] = drawIcon; -jdenticon["toSvg"] = toSvg; -jdenticon["update"] = update; -jdenticon["updateCanvas"] = update; -jdenticon["updateSvg"] = update; - -/** - * Specifies the version of the Jdenticon package in use. - * @type {string} - */ -jdenticon["version"] = "3.2.0"; - -/** - * Specifies which bundle of Jdenticon that is used. - * @type {string} - */ -jdenticon["bundle"] = "browser-umd"; - -// Basic jQuery plugin -var jQuery = GLOBAL["jQuery"]; -if (jQuery) { - jQuery["fn"]["jdenticon"] = jdenticonJqueryPlugin; -} - -/** - * This function is called once upon page load. - */ -function jdenticonStartup() { - var replaceMode = ( - jdenticon[CONFIG_PROPERTIES.n/*MODULE*/] || - GLOBAL[CONFIG_PROPERTIES.G/*GLOBAL*/] || - { } - )["replaceMode"]; - - if (replaceMode != "never") { - updateAll(); - - if (replaceMode == "observe") { - observer(update); - } - } -} - -// Schedule to render all identicons on the page once it has been loaded. -if (typeof setTimeout === "function") { - setTimeout(jdenticonStartup, 0); -} - -return jdenticon; - -}); -\ No newline at end of file diff --git a/src/static/scripts/jquery-3.7.0.slim.js b/src/static/scripts/jquery-3.7.0.slim.js @@ -1,8605 +0,0 @@ -/*! - * jQuery JavaScript Library v3.7.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/animatedSelector,-effects/Tween - * https://jquery.com/ - * - * Copyright OpenJS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2023-05-11T18:29Z - */ -( function( global, factory ) { - - "use strict"; - - if ( typeof module === "object" && typeof module.exports === "object" ) { - - // For CommonJS and CommonJS-like environments where a proper `window` - // is present, execute the factory and get jQuery. - // For environments that do not have a `window` with a `document` - // (such as Node.js), expose a factory as module.exports. - // This accentuates the need for the creation of a real `window`. - // e.g. var jQuery = require("jquery")(window); - // See ticket trac-14549 for more info. - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 -// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode -// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common -// enough that all such attempts are guarded in a try block. -"use strict"; - -var arr = []; - -var getProto = Object.getPrototypeOf; - -var slice = arr.slice; - -var flat = arr.flat ? function( array ) { - return arr.flat.call( array ); -} : function( array ) { - return arr.concat.apply( [], array ); -}; - - -var push = arr.push; - -var indexOf = arr.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var fnToString = hasOwn.toString; - -var ObjectFunctionString = fnToString.call( Object ); - -var support = {}; - -var isFunction = function isFunction( obj ) { - - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML <object> elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 - // Plus for old WebKit, typeof returns "function" for HTML collections - // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) - return typeof obj === "function" && typeof obj.nodeType !== "number" && - typeof obj.item !== "function"; - }; - - -var isWindow = function isWindow( obj ) { - return obj != null && obj === obj.window; - }; - - -var document = window.document; - - - - var preservedScriptAttributes = { - type: true, - src: true, - nonce: true, - noModule: true - }; - - function DOMEval( code, node, doc ) { - doc = doc || document; - - var i, val, - script = doc.createElement( "script" ); - - script.text = code; - if ( node ) { - for ( i in preservedScriptAttributes ) { - - // Support: Firefox 64+, Edge 18+ - // Some browsers don't support the "nonce" property on scripts. - // On the other hand, just using `getAttribute` is not enough as - // the `nonce` attribute is reset to an empty string whenever it - // becomes browsing-context connected. - // See https://github.com/whatwg/html/issues/2369 - // See https://html.spec.whatwg.org/#nonce-attributes - // The `node.getAttribute` check was added for the sake of - // `jQuery.globalEval` so that it can fake a nonce-containing node - // via an object. - val = node[ i ] || node.getAttribute && node.getAttribute( i ); - if ( val ) { - script.setAttribute( i, val ); - } - } - } - doc.head.appendChild( script ).parentNode.removeChild( script ); - } - - -function toType( obj ) { - if ( obj == null ) { - return obj + ""; - } - - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; -} -/* global Symbol */ -// Defining this global in .eslintrc.json would create a danger of using the global -// unguarded in another place, it seems safer to define global only for this module - - - -var version = "3.7.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/animatedSelector,-effects/Tween", - - rhtmlSuffix = /HTML$/i, - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }; - -jQuery.fn = jQuery.prototype = { - - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - - // Return all the elements in a clean array - if ( num == null ) { - return slice.call( this ); - } - - // Return just the one element from the set - return num < 0 ? this[ num + this.length ] : this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - each: function( callback ) { - return jQuery.each( this, callback ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map( this, function( elem, i ) { - return callback.call( elem, i, elem ); - } ) ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - even: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return ( i + 1 ) % 2; - } ) ); - }, - - odd: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return i % 2; - } ) ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: arr.sort, - splice: arr.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[ 0 ] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // Skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !isFunction( target ) ) { - target = {}; - } - - // Extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - - // Only deal with non-null/undefined values - if ( ( options = arguments[ i ] ) != null ) { - - // Extend the base object - for ( name in options ) { - copy = options[ name ]; - - // Prevent Object.prototype pollution - // Prevent never-ending loop - if ( name === "__proto__" || target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject( copy ) || - ( copyIsArray = Array.isArray( copy ) ) ) ) { - src = target[ name ]; - - // Ensure proper type for the source value - if ( copyIsArray && !Array.isArray( src ) ) { - clone = []; - } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { - clone = {}; - } else { - clone = src; - } - copyIsArray = false; - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend( { - - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - isPlainObject: function( obj ) { - var proto, Ctor; - - // Detect obvious negatives - // Use toString instead of jQuery.type to catch host objects - if ( !obj || toString.call( obj ) !== "[object Object]" ) { - return false; - } - - proto = getProto( obj ); - - // Objects with no prototype (e.g., `Object.create( null )`) are plain - if ( !proto ) { - return true; - } - - // Objects with prototype are plain iff they were constructed by a global Object function - Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; - return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; - }, - - isEmptyObject: function( obj ) { - var name; - - for ( name in obj ) { - return false; - } - return true; - }, - - // Evaluates a script in a provided context; falls back to the global one - // if not specified. - globalEval: function( code, options, doc ) { - DOMEval( code, { nonce: options && options.nonce }, doc ); - }, - - each: function( obj, callback ) { - var length, i = 0; - - if ( isArrayLike( obj ) ) { - length = obj.length; - for ( ; i < length; i++ ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } else { - for ( i in obj ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } - - return obj; - }, - - - // Retrieve the text value of an array of DOM nodes - text: function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - - // If no nodeType, this is expected to be an array - while ( ( node = elem[ i++ ] ) ) { - - // Do not traverse comment nodes - ret += jQuery.text( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - return elem.textContent; - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - - // Do not include comment or processing instruction nodes - - return ret; - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArrayLike( Object( arr ) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - return arr == null ? -1 : indexOf.call( arr, elem, i ); - }, - - isXMLDoc: function( elem ) { - var namespace = elem && elem.namespaceURI, - docElem = elem && ( elem.ownerDocument || elem ).documentElement; - - // Assume HTML when documentElement doesn't yet exist, such as inside - // document fragments. - return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); - }, - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - for ( ; j < len; j++ ) { - first[ i++ ] = second[ j ]; - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var length, value, - i = 0, - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArrayLike( elems ) ) { - length = elems.length; - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return flat( ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -} ); - -if ( typeof Symbol === "function" ) { - jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; -} - -// Populate the class2type map -jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), - function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); - } ); - -function isArrayLike( obj ) { - - // Support: real iOS 8.2 only (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = !!obj && "length" in obj && obj.length, - type = toType( obj ); - - if ( isFunction( obj ) || isWindow( obj ) ) { - return false; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} - - -function nodeName( elem, name ) { - - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - -} -var pop = arr.pop; - - -var sort = arr.sort; - - -var splice = arr.splice; - - -var whitespace = "[\\x20\\t\\r\\n\\f]"; - - -var rtrimCSS = new RegExp( - "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", - "g" -); - - - - -// Note: an element does not contain itself -jQuery.contains = function( a, b ) { - var bup = b && b.parentNode; - - return a === bup || !!( bup && bup.nodeType === 1 && ( - - // Support: IE 9 - 11+ - // IE doesn't have `contains` on SVG. - a.contains ? - a.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - ) ); -}; - - - - -// CSS string/identifier serialization -// https://drafts.csswg.org/cssom/#common-serializing-idioms -var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; - -function fcssescape( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; -} - -jQuery.escapeSelector = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); -}; - - - - -var preferredDoc = document, - pushNative = push; - -( function() { - -var i, - Expr, - outermostContext, - sortInput, - hasDuplicate, - push = pushNative, - - // Local document vars - document, - documentElement, - documentIsHTML, - rbuggyQSA, - matches, - - // Instance-specific data - expando = jQuery.expando, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - nonnativeSelectorCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + - "loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram - identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", - - // Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + - whitespace + "*\\]", - - pseudos = ":(" + identifier + ")(?:\\((" + - - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + - whitespace + "*" ), - rdescend = new RegExp( whitespace + "|>" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - ID: new RegExp( "^#(" + identifier + ")" ), - CLASS: new RegExp( "^\\.(" + identifier + ")" ), - TAG: new RegExp( "^(" + identifier + "|[*])" ), - ATTR: new RegExp( "^" + attributes ), - PSEUDO: new RegExp( "^" + pseudos ), - CHILD: new RegExp( - "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + - whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + - whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - bool: new RegExp( "^(?:" + booleans + ")$", "i" ), - - // For use in libraries implementing .is() - // We use this for POS matching in `select` - needsContext: new RegExp( "^" + whitespace + - "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + - "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - - // CSS escapes - // https://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\([^\\r\\n\\f])", "g" ), - funescape = function( escape, nonHex ) { - var high = "0x" + escape.slice( 1 ) - 0x10000; - - if ( nonHex ) { - - // Strip the backslash prefix from a non-hex escape sequence - return nonHex; - } - - // Replace a hexadecimal escape sequence with the encoded Unicode code point - // Support: IE <=11+ - // For values outside the Basic Multilingual Plane (BMP), manually construct a - // surrogate pair - return high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - - // Used for iframes; see `setDocument`. - // Support: IE 9 - 11+, Edge 12 - 18+ - // Removing the function wrapper causes a "Permission Denied" - // error in IE/Edge. - unloadHandler = function() { - setDocument(); - }, - - inDisabledFieldset = addCombinator( - function( elem ) { - return elem.disabled === true && nodeName( elem, "fieldset" ); - }, - { dir: "parentNode", next: "legend" } - ); - -// Support: IE <=9 only -// Accessing document.activeElement can throw unexpectedly -// https://bugs.jquery.com/ticket/13393 -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - ( arr = slice.call( preferredDoc.childNodes ) ), - preferredDoc.childNodes - ); - - // Support: Android <=4.0 - // Detect silently failing push.apply - // eslint-disable-next-line no-unused-expressions - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { - apply: function( target, els ) { - pushNative.apply( target, slice.call( els ) ); - }, - call: function( target ) { - pushNative.apply( target, slice.call( arguments, 1 ) ); - } - }; -} - -function find( selector, context, results, seed ) { - var m, i, elem, nid, match, groups, newSelector, - newContext = context && context.ownerDocument, - - // nodeType defaults to 9, since context defaults to document - nodeType = context ? context.nodeType : 9; - - results = results || []; - - // Return early from calls with invalid selector or context - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - // Try to shortcut find operations (as opposed to filters) in HTML documents - if ( !seed ) { - setDocument( context ); - context = context || document; - - if ( documentIsHTML ) { - - // If the selector is sufficiently simple, try using a "get*By*" DOM method - // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { - - // ID selector - if ( ( m = match[ 1 ] ) ) { - - // Document context - if ( nodeType === 9 ) { - if ( ( elem = context.getElementById( m ) ) ) { - - // Support: IE 9 only - // getElementById can match elements by name instead of ID - if ( elem.id === m ) { - push.call( results, elem ); - return results; - } - } else { - return results; - } - - // Element context - } else { - - // Support: IE 9 only - // getElementById can match elements by name instead of ID - if ( newContext && ( elem = newContext.getElementById( m ) ) && - find.contains( context, elem ) && - elem.id === m ) { - - push.call( results, elem ); - return results; - } - } - - // Type selector - } else if ( match[ 2 ] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Class selector - } else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // Take advantage of querySelectorAll - if ( !nonnativeSelectorCache[ selector + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) { - - newSelector = selector; - newContext = context; - - // qSA considers elements outside a scoping root when evaluating child or - // descendant combinators, which is not what we want. - // In such cases, we work around the behavior by prefixing every selector in the - // list with an ID selector referencing the scope context. - // The technique has to be used as well when a leading combinator is used - // as such selectors are not recognized by querySelectorAll. - // Thanks to Andrew Dupont for this technique. - if ( nodeType === 1 && - ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || - context; - - // We can use :scope instead of the ID hack if the browser - // supports it & if we're not changing the context. - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when - // strict-comparing two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( newContext != context || !support.scope ) { - - // Capture the context ID, setting it first if necessary - if ( ( nid = context.getAttribute( "id" ) ) ) { - nid = jQuery.escapeSelector( nid ); - } else { - context.setAttribute( "id", ( nid = expando ) ); - } - } - - // Prefix every selector in the list - groups = tokenize( selector ); - i = groups.length; - while ( i-- ) { - groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + - toSelector( groups[ i ] ); - } - newSelector = groups.join( "," ); - } - - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - nonnativeSelectorCache( selector, true ); - } finally { - if ( nid === expando ) { - context.removeAttribute( "id" ); - } - } - } - } - } - - // All others - return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - - // Use (key + " ") to avoid collision with native prototype properties - // (see https://github.com/jquery/sizzle/issues/157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return ( cache[ key + " " ] = value ); - } - return cache; -} - -/** - * Mark a function for special use by jQuery selector module - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created element and returns a boolean result - */ -function assert( fn ) { - var el = document.createElement( "fieldset" ); - - try { - return !!fn( el ); - } catch ( e ) { - return false; - } finally { - - // Remove from its parent by default - if ( el.parentNode ) { - el.parentNode.removeChild( el ); - } - - // release memory in IE - el = null; - } -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - return nodeName( elem, "input" ) && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - return ( nodeName( elem, "input" ) || nodeName( elem, "button" ) ) && - elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for :enabled/:disabled - * @param {Boolean} disabled true for :disabled; false for :enabled - */ -function createDisabledPseudo( disabled ) { - - // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable - return function( elem ) { - - // Only certain elements can match :enabled or :disabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled - if ( "form" in elem ) { - - // Check for inherited disabledness on relevant non-disabled elements: - // * listed form-associated elements in a disabled fieldset - // https://html.spec.whatwg.org/multipage/forms.html#category-listed - // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled - // * option elements in a disabled optgroup - // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled - // All such elements have a "form" property. - if ( elem.parentNode && elem.disabled === false ) { - - // Option elements defer to a parent optgroup if present - if ( "label" in elem ) { - if ( "label" in elem.parentNode ) { - return elem.parentNode.disabled === disabled; - } else { - return elem.disabled === disabled; - } - } - - // Support: IE 6 - 11+ - // Use the isDisabled shortcut property to check for disabled fieldset ancestors - return elem.isDisabled === disabled || - - // Where there is no isDisabled, check manually - elem.isDisabled !== !disabled && - inDisabledFieldset( elem ) === disabled; - } - - return elem.disabled === disabled; - - // Try to winnow out elements that can't be disabled before trusting the disabled property. - // Some victims get caught in our net (label, legend, menu, track), but it shouldn't - // even exist on them, let alone have a boolean value. - } else if ( "label" in elem ) { - return elem.disabled === disabled; - } - - // Remaining elements are neither :enabled nor :disabled - return false; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction( function( argument ) { - argument = +argument; - return markFunction( function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ ( j = matchIndexes[ i ] ) ] ) { - seed[ j ] = !( matches[ j ] = seed[ j ] ); - } - } - } ); - } ); -} - -/** - * Checks a node for validity as a jQuery selector context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [node] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -function setDocument( node ) { - var subWindow, - doc = node ? node.ownerDocument || node : preferredDoc; - - // Return early if doc is invalid or already selected - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Update global variables - document = doc; - documentElement = document.documentElement; - documentIsHTML = !jQuery.isXMLDoc( document ); - - // Support: iOS 7 only, IE 9 - 11+ - // Older browsers didn't support unprefixed `matches`. - matches = documentElement.matches || - documentElement.webkitMatchesSelector || - documentElement.msMatchesSelector; - - // Support: IE 9 - 11+, Edge 12 - 18+ - // Accessing iframe documents after unload throws "permission denied" errors (see trac-13936) - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( preferredDoc != document && - ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { - - // Support: IE 9 - 11+, Edge 12 - 18+ - subWindow.addEventListener( "unload", unloadHandler ); - } - - // Support: IE <10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programmatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert( function( el ) { - documentElement.appendChild( el ).id = jQuery.expando; - return !document.getElementsByName || - !document.getElementsByName( jQuery.expando ).length; - } ); - - // Support: IE 9 only - // Check to see if it's possible to do matchesSelector - // on a disconnected node. - support.disconnectedMatch = assert( function( el ) { - return matches.call( el, "*" ); - } ); - - // Support: IE 9 - 11+, Edge 12 - 18+ - // IE/Edge don't support the :scope pseudo-class. - support.scope = assert( function() { - return document.querySelectorAll( ":scope" ); - } ); - - // Support: Chrome 105 - 111 only, Safari 15.4 - 16.3 only - // Make sure the `:has()` argument is parsed unforgivingly. - // We include `*` in the test to detect buggy implementations that are - // _selectively_ forgiving (specifically when the list includes at least - // one valid selector). - // Note that we treat complete lack of support for `:has()` as if it were - // spec-compliant support, which is fine because use of `:has()` in such - // environments will fail in the qSA path and fall back to jQuery traversal - // anyway. - support.cssHas = assert( function() { - try { - document.querySelector( ":has(*,:jqfake)" ); - return false; - } catch ( e ) { - return true; - } - } ); - - // ID filter and find - if ( support.getById ) { - Expr.filter.ID = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute( "id" ) === attrId; - }; - }; - Expr.find.ID = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var elem = context.getElementById( id ); - return elem ? [ elem ] : []; - } - }; - } else { - Expr.filter.ID = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && - elem.getAttributeNode( "id" ); - return node && node.value === attrId; - }; - }; - - // Support: IE 6 - 7 only - // getElementById is not reliable as a find shortcut - Expr.find.ID = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var node, i, elems, - elem = context.getElementById( id ); - - if ( elem ) { - - // Verify the id attribute - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - - // Fall back on getElementsByName - elems = context.getElementsByName( id ); - i = 0; - while ( ( elem = elems[ i++ ] ) ) { - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - } - } - - return []; - } - }; - } - - // Tag - Expr.find.TAG = function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else { - return context.querySelectorAll( tag ); - } - }; - - // Class - Expr.find.CLASS = function( className, context ) { - if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - rbuggyQSA = []; - - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert( function( el ) { - - var input; - - documentElement.appendChild( el ).innerHTML = - "<a id='" + expando + "' href='' disabled='disabled'></a>" + - "<select id='" + expando + "-\r\\' disabled='disabled'>" + - "<option selected=''></option></select>"; - - // Support: iOS <=7 - 8 only - // Boolean attributes and "value" are not treated correctly in some XML documents - if ( !el.querySelectorAll( "[selected]" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: iOS <=7 - 8 only - if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push( "~=" ); - } - - // Support: iOS 8 only - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibling-combinator selector` fails - if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push( ".#.+[+~]" ); - } - - // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ - // In some of the document kinds, these selectors wouldn't work natively. - // This is probably OK but for backwards compatibility we want to maintain - // handling them through jQuery traversal in jQuery 3.x. - if ( !el.querySelectorAll( ":checked" ).length ) { - rbuggyQSA.push( ":checked" ); - } - - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - input = document.createElement( "input" ); - input.setAttribute( "type", "hidden" ); - el.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE 9 - 11+ - // IE's :disabled selector does not pick up the children of disabled fieldsets - // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ - // In some of the document kinds, these selectors wouldn't work natively. - // This is probably OK but for backwards compatibility we want to maintain - // handling them through jQuery traversal in jQuery 3.x. - documentElement.appendChild( el ).disabled = true; - if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: IE 11+, Edge 15 - 18+ - // IE 11/Edge don't find elements on a `[name='']` query in some cases. - // Adding a temporary attribute to the document before the selection works - // around the issue. - // Interestingly, IE 10 & older don't seem to have the issue. - input = document.createElement( "input" ); - input.setAttribute( "name", "" ); - el.appendChild( input ); - if ( !el.querySelectorAll( "[name='']" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + - whitespace + "*(?:''|\"\")" ); - } - } ); - - if ( !support.cssHas ) { - - // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ - // Our regular `try-catch` mechanism fails to detect natively-unsupported - // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`) - // in browsers that parse the `:has()` argument as a forgiving selector list. - // https://drafts.csswg.org/selectors/#relational now requires the argument - // to be parsed unforgivingly, but browsers have not yet fully adjusted. - rbuggyQSA.push( ":has" ); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { - - // Choose the first element that is related to our preferred document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( a === document || a.ownerDocument == preferredDoc && - find.contains( preferredDoc, a ) ) { - return -1; - } - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( b === document || b.ownerDocument == preferredDoc && - find.contains( preferredDoc, b ) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - }; - - return document; -} - -find.matches = function( expr, elements ) { - return find( expr, null, null, elements ); -}; - -find.matchesSelector = function( elem, expr ) { - setDocument( elem ); - - if ( documentIsHTML && - !nonnativeSelectorCache[ expr + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch ( e ) { - nonnativeSelectorCache( expr, true ); - } - } - - return find( expr, document, null, [ elem ] ).length > 0; -}; - -find.contains = function( context, elem ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( context.ownerDocument || context ) != document ) { - setDocument( context ); - } - return jQuery.contains( context, elem ); -}; - - -find.attr = function( elem, name ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( elem.ownerDocument || elem ) != document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - - // Don't get fooled by Object.prototype properties (see trac-13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - if ( val !== undefined ) { - return val; - } - - return elem.getAttribute( name ); -}; - -find.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -jQuery.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - // - // Support: Android <=4.0+ - // Testing for detecting duplicates is unpredictable so instead assume we can't - // depend on duplicate detection in all browsers without a stable sort. - hasDuplicate = !support.sortStable; - sortInput = !support.sortStable && slice.call( results, 0 ); - sort.call( results, sortOrder ); - - if ( hasDuplicate ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - splice.call( results, duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -jQuery.fn.uniqueSort = function() { - return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); -}; - -Expr = jQuery.expr = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - ATTR: function( match ) { - match[ 1 ] = match[ 1 ].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) - .replace( runescape, funescape ); - - if ( match[ 2 ] === "~=" ) { - match[ 3 ] = " " + match[ 3 ] + " "; - } - - return match.slice( 0, 4 ); - }, - - CHILD: function( match ) { - - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[ 1 ] = match[ 1 ].toLowerCase(); - - if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - - // nth-* requires argument - if ( !match[ 3 ] ) { - find.error( match[ 0 ] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[ 4 ] = +( match[ 4 ] ? - match[ 5 ] + ( match[ 6 ] || 1 ) : - 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) - ); - match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - - // other types prohibit arguments - } else if ( match[ 3 ] ) { - find.error( match[ 0 ] ); - } - - return match; - }, - - PSEUDO: function( match ) { - var excess, - unquoted = !match[ 6 ] && match[ 2 ]; - - if ( matchExpr.CHILD.test( match[ 0 ] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[ 3 ] ) { - match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - - // Get excess from tokenize (recursively) - ( excess = tokenize( unquoted, true ) ) && - - // advance to the next closing parenthesis - ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { - - // excess is a negative index - match[ 0 ] = match[ 0 ].slice( 0, excess ); - match[ 2 ] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - TAG: function( nodeNameSelector ) { - var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { - return true; - } : - function( elem ) { - return nodeName( elem, expectedNodeName ); - }; - }, - - CLASS: function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - ( pattern = new RegExp( "(^|" + whitespace + ")" + className + - "(" + whitespace + "|$)" ) ) && - classCache( className, function( elem ) { - return pattern.test( - typeof elem.className === "string" && elem.className || - typeof elem.getAttribute !== "undefined" && - elem.getAttribute( "class" ) || - "" - ); - } ); - }, - - ATTR: function( name, operator, check ) { - return function( elem ) { - var result = find.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - if ( operator === "=" ) { - return result === check; - } - if ( operator === "!=" ) { - return result !== check; - } - if ( operator === "^=" ) { - return check && result.indexOf( check ) === 0; - } - if ( operator === "*=" ) { - return check && result.indexOf( check ) > -1; - } - if ( operator === "$=" ) { - return check && result.slice( -check.length ) === check; - } - if ( operator === "~=" ) { - return ( " " + result.replace( rwhitespace, " " ) + " " ) - .indexOf( check ) > -1; - } - if ( operator === "|=" ) { - return result === check || result.slice( 0, check.length + 1 ) === check + "-"; - } - - return false; - }; - }, - - CHILD: function( type, what, _argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, _context, xml ) { - var cache, outerCache, node, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType, - diff = false; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( ( node = node[ dir ] ) ) { - if ( ofType ? - nodeName( node, name ) : - node.nodeType === 1 ) { - - return false; - } - } - - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || ( parent[ expando ] = {} ); - cache = outerCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex && cache[ 2 ]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( ( node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - } else { - - // Use previously-cached element index if available - if ( useCache ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); - cache = outerCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex; - } - - // xml :nth-child(...) - // or :nth-last-child(...) or :nth(-last)?-of-type(...) - if ( diff === false ) { - - // Use the same loop as above to seek `elem` from the start - while ( ( node = ++nodeIndex && node && node[ dir ] || - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - if ( ( ofType ? - nodeName( node, name ) : - node.nodeType === 1 ) && - ++diff ) { - - // Cache the index of each encountered element - if ( useCache ) { - outerCache = node[ expando ] || - ( node[ expando ] = {} ); - outerCache[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - PSEUDO: function( pseudo, argument ) { - - // pseudo-class names are case-insensitive - // https://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - find.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as jQuery does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction( function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[ i ] ); - seed[ idx ] = !( matches[ idx ] = matched[ i ] ); - } - } ) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - - // Potentially complex pseudos - not: markFunction( function( selector ) { - - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrimCSS, "$1" ) ); - - return matcher[ expando ] ? - markFunction( function( seed, matches, _context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( ( elem = unmatched[ i ] ) ) { - seed[ i ] = !( matches[ i ] = elem ); - } - } - } ) : - function( elem, _context, xml ) { - input[ 0 ] = elem; - matcher( input, null, xml, results ); - - // Don't keep the element - // (see https://github.com/jquery/sizzle/issues/299) - input[ 0 ] = null; - return !results.pop(); - }; - } ), - - has: markFunction( function( selector ) { - return function( elem ) { - return find( selector, elem ).length > 0; - }; - } ), - - contains: markFunction( function( text ) { - text = text.replace( runescape, funescape ); - return function( elem ) { - return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; - }; - } ), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // https://www.w3.org/TR/selectors/#lang-pseudo - lang: markFunction( function( lang ) { - - // lang value must be a valid identifier - if ( !ridentifier.test( lang || "" ) ) { - find.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( ( elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); - return false; - }; - } ), - - // Miscellaneous - target: function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - root: function( elem ) { - return elem === documentElement; - }, - - focus: function( elem ) { - return elem === safeActiveElement() && - document.hasFocus() && - !!( elem.type || elem.href || ~elem.tabIndex ); - }, - - // Boolean properties - enabled: createDisabledPseudo( false ), - disabled: createDisabledPseudo( true ), - - checked: function( elem ) { - - // In CSS3, :checked should return both checked and selected elements - // https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - return ( nodeName( elem, "input" ) && !!elem.checked ) || - ( nodeName( elem, "option" ) && !!elem.selected ); - }, - - selected: function( elem ) { - - // Support: IE <=11+ - // Accessing the selectedIndex property - // forces the browser to treat the default option as - // selected when in an optgroup. - if ( elem.parentNode ) { - // eslint-disable-next-line no-unused-expressions - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - empty: function( elem ) { - - // https://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - parent: function( elem ) { - return !Expr.pseudos.empty( elem ); - }, - - // Element/input types - header: function( elem ) { - return rheader.test( elem.nodeName ); - }, - - input: function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - button: function( elem ) { - return nodeName( elem, "input" ) && elem.type === "button" || - nodeName( elem, "button" ); - }, - - text: function( elem ) { - var attr; - return nodeName( elem, "input" ) && elem.type === "text" && - - // Support: IE <10 only - // New HTML5 attribute values (e.g., "search") appear - // with elem.type === "text" - ( ( attr = elem.getAttribute( "type" ) ) == null || - attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - first: createPositionalPseudo( function() { - return [ 0 ]; - } ), - - last: createPositionalPseudo( function( _matchIndexes, length ) { - return [ length - 1 ]; - } ), - - eq: createPositionalPseudo( function( _matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - } ), - - even: createPositionalPseudo( function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - odd: createPositionalPseudo( function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - lt: createPositionalPseudo( function( matchIndexes, length, argument ) { - var i; - - if ( argument < 0 ) { - i = argument + length; - } else if ( argument > length ) { - i = length; - } else { - i = argument; - } - - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - gt: createPositionalPseudo( function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ) - } -}; - -Expr.pseudos.nth = Expr.pseudos.eq; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || ( match = rcomma.exec( soFar ) ) ) { - if ( match ) { - - // Don't consume trailing commas as valid - soFar = soFar.slice( match[ 0 ].length ) || soFar; - } - groups.push( ( tokens = [] ) ); - } - - matched = false; - - // Combinators - if ( ( match = rleadingCombinator.exec( soFar ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - - // Cast descendant combinators to space - type: match[ 0 ].replace( rtrimCSS, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || - ( match = preFilters[ type ]( match ) ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - if ( parseOnly ) { - return soFar.length; - } - - return soFar ? - find.error( selector ) : - - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[ i ].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - skip = combinator.next, - key = skip || dir, - checkNonElements = base && key === "parentNode", - doneName = done++; - - return combinator.first ? - - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - return false; - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching - if ( xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); - - if ( skip && nodeName( elem, skip ) ) { - elem = elem[ dir ] || elem; - } else if ( ( oldCache = outerCache[ key ] ) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return ( newCache[ 2 ] = oldCache[ 2 ] ); - } else { - - // Reuse newcache so results back-propagate to previous elements - outerCache[ key ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { - return true; - } - } - } - } - } - return false; - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[ i ]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[ 0 ]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - find( selector, contexts[ i ], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( ( elem = unmatched[ i ] ) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction( function( seed, results, context, xml ) { - var temp, i, elem, matcherOut, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || - multipleContexts( selector || "*", - context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems; - - if ( matcher ) { - - // If we have a postFinder, or filtered seed, or non-seed postFilter - // or preexisting results, - matcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results; - - // Find primary matches - matcher( matcherIn, matcherOut, context, xml ); - } else { - matcherOut = matcherIn; - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( ( elem = temp[ i ] ) ) { - matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) ) { - - // Restore matcherIn since elem is not yet a final match - temp.push( ( matcherIn[ i ] = elem ) ); - } - } - postFinder( null, ( matcherOut = [] ), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) && - ( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) { - - seed[ temp ] = !( results[ temp ] = elem ); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - } ); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[ 0 ].type ], - implicitRelative = leadingRelative || Expr.relative[ " " ], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( - ( checkContext = context ).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - - // Avoid hanging onto element - // (see https://github.com/jquery/sizzle/issues/299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { - matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; - } else { - matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[ j ].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ) - .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) - ).replace( rtrimCSS, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find.TAG( "*", outermost ), - - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), - len = elems.length; - - if ( outermost ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - outermostContext = context == document || context || outermost; - } - - // Add elements passing elementMatchers directly to results - // Support: iOS <=7 - 9 only - // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching - // elements by id. (see trac-14142) - for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( !context && elem.ownerDocument != document ) { - setDocument( elem ); - xml = !documentIsHTML; - } - while ( ( matcher = elementMatchers[ j++ ] ) ) { - if ( matcher( elem, context || document, xml ) ) { - push.call( results, elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - - // They will have gone through all possible matchers - if ( ( elem = !matcher && elem ) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // `i` is now the count of elements visited above, and adding it to `matchedCount` - // makes the latter nonnegative. - matchedCount += i; - - // Apply set filters to unmatched elements - // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` - // equals `i`), unless we didn't visit _any_ elements in the above loop because we have - // no element matchers and no seed. - // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that - // case, which will result in a "00" `matchedCount` that differs from `i` but is also - // numerically zero. - if ( bySet && i !== matchedCount ) { - j = 0; - while ( ( matcher = setMatchers[ j++ ] ) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !( unmatched[ i ] || setMatched[ i ] ) ) { - setMatched[ i ] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - jQuery.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -function compile( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[ i ] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, - matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -} - -/** - * A low-level selection function that works with jQuery's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with jQuery selector compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -function select( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( ( selector = compiled.selector || selector ) ); - - results = results || []; - - // Try to minimize operations if there is only one selector in the list and no seed - // (the latter of which guarantees us context) - if ( match.length === 1 ) { - - // Reduce context if the leading compound selector is an ID - tokens = match[ 0 ] = match[ 0 ].slice( 0 ); - if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { - - context = ( Expr.find.ID( - token.matches[ 0 ].replace( runescape, funescape ), - context - ) || [] )[ 0 ]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr.needsContext.test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[ i ]; - - // Abort if we hit a combinator - if ( Expr.relative[ ( type = token.type ) ] ) { - break; - } - if ( ( find = Expr.find[ type ] ) ) { - - // Search, expanding context for leading sibling combinators - if ( ( seed = find( - token.matches[ 0 ].replace( runescape, funescape ), - rsibling.test( tokens[ 0 ].type ) && - testContext( context.parentNode ) || context - ) ) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - !context || rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -} - -// One-time assignments - -// Support: Android <=4.0 - 4.1+ -// Sort stability -support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; - -// Initialize against the default document -setDocument(); - -// Support: Android <=4.0 - 4.1+ -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert( function( el ) { - - // Should return 1, but returns 4 (following) - return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; -} ); - -jQuery.find = find; - -// Deprecated -jQuery.expr[ ":" ] = jQuery.expr.pseudos; -jQuery.unique = jQuery.uniqueSort; - -// These have always been private, but they used to be documented -// as part of Sizzle so let's maintain them in the 3.x line -// for backwards compatibility purposes. -find.compile = compile; -find.select = select; -find.setDocument = setDocument; - -find.escape = jQuery.escapeSelector; -find.getText = jQuery.text; -find.isXML = jQuery.isXMLDoc; -find.selectors = jQuery.expr; -find.support = jQuery.support; -find.uniqueSort = jQuery.uniqueSort; - - /* eslint-enable */ - -} )(); - - -var dir = function( elem, dir, until ) { - var matched = [], - truncate = until !== undefined; - - while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { - if ( elem.nodeType === 1 ) { - if ( truncate && jQuery( elem ).is( until ) ) { - break; - } - matched.push( elem ); - } - } - return matched; -}; - - -var siblings = function( n, elem ) { - var matched = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - matched.push( n ); - } - } - - return matched; -}; - - -var rneedsContext = jQuery.expr.match.needsContext; - -var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); - - - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - return !!qualifier.call( elem, i, elem ) !== not; - } ); - } - - // Single element - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - } ); - } - - // Arraylike of elements (jQuery, arguments, Array) - if ( typeof qualifier !== "string" ) { - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) > -1 ) !== not; - } ); - } - - // Filtered directly for both simple and complex selectors - return jQuery.filter( qualifier, elements, not ); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - if ( elems.length === 1 && elem.nodeType === 1 ) { - return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; - } - - return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - } ) ); -}; - -jQuery.fn.extend( { - find: function( selector ) { - var i, ret, - len = this.length, - self = this; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter( function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - } ) ); - } - - ret = this.pushStack( [] ); - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - return len > 1 ? jQuery.uniqueSort( ret ) : ret; - }, - filter: function( selector ) { - return this.pushStack( winnow( this, selector || [], false ) ); - }, - not: function( selector ) { - return this.pushStack( winnow( this, selector || [], true ) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -} ); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // A simple way to check for HTML strings - // Prioritize #id over <tag> to avoid XSS via location.hash (trac-9521) - // Strict HTML recognition (trac-11290: must start with <) - // Shortcut simple #id case for speed - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, - - init = jQuery.fn.init = function( selector, context, root ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Method init() accepts an alternate rootjQuery - // so migrate can support jQuery.sub (gh-2101) - root = root || rootjQuery; - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector[ 0 ] === "<" && - selector[ selector.length - 1 ] === ">" && - selector.length >= 3 ) { - - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && ( match[ 1 ] || !context ) ) { - - // HANDLE: $(html) -> $(array) - if ( match[ 1 ] ) { - context = context instanceof jQuery ? context[ 0 ] : context; - - // Option to run scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[ 1 ], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - - // Properties of context are called as methods if possible - if ( isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[ 2 ] ); - - if ( elem ) { - - // Inject the element directly into the jQuery object - this[ 0 ] = elem; - this.length = 1; - } - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || root ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this[ 0 ] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( isFunction( selector ) ) { - return root.ready !== undefined ? - root.ready( selector ) : - - // Execute immediately if ready is not present - selector( jQuery ); - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - - // Methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend( { - has: function( target ) { - var targets = jQuery( target, this ), - l = targets.length; - - return this.filter( function() { - var i = 0; - for ( ; i < l; i++ ) { - if ( jQuery.contains( this, targets[ i ] ) ) { - return true; - } - } - } ); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - targets = typeof selectors !== "string" && jQuery( selectors ); - - // Positional selectors never match, since there's no _selection_ context - if ( !rneedsContext.test( selectors ) ) { - for ( ; i < l; i++ ) { - for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { - - // Always skip document fragments - if ( cur.nodeType < 11 && ( targets ? - targets.index( cur ) > -1 : - - // Don't pass non-elements to jQuery#find - cur.nodeType === 1 && - jQuery.find.matchesSelector( cur, selectors ) ) ) { - - matched.push( cur ); - break; - } - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); - }, - - // Determine the position of an element within the set - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; - } - - // Index in selector - if ( typeof elem === "string" ) { - return indexOf.call( jQuery( elem ), this[ 0 ] ); - } - - // Locate the position of the desired element - return indexOf.call( this, - - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[ 0 ] : elem - ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.uniqueSort( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - } -} ); - -function sibling( cur, dir ) { - while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} - return cur; -} - -jQuery.each( { - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, _i, until ) { - return dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, _i, until ) { - return dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, _i, until ) { - return dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return siblings( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return siblings( elem.firstChild ); - }, - contents: function( elem ) { - if ( elem.contentDocument != null && - - // Support: IE 11+ - // <object> elements with no `data` attribute has an object - // `contentDocument` with a `null` prototype. - getProto( elem.contentDocument ) ) { - - return elem.contentDocument; - } - - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only - // Treat the template element as a regular one in browsers that - // don't support it. - if ( nodeName( elem, "template" ) ) { - elem = elem.content || elem; - } - - return jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var matched = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - matched = jQuery.filter( selector, matched ); - } - - if ( this.length > 1 ) { - - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - jQuery.uniqueSort( matched ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - matched.reverse(); - } - } - - return this.pushStack( matched ); - }; -} ); -var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); - - - -// Convert String-formatted options into Object-formatted ones -function createOptions( options ) { - var object = {}; - jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { - object[ flag ] = true; - } ); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - createOptions( options ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - - // Last fire value for non-forgettable lists - memory, - - // Flag to know if list was already fired - fired, - - // Flag to prevent firing - locked, - - // Actual callback list - list = [], - - // Queue of execution data for repeatable lists - queue = [], - - // Index of currently firing callback (modified by add/remove as needed) - firingIndex = -1, - - // Fire callbacks - fire = function() { - - // Enforce single-firing - locked = locked || options.once; - - // Execute callbacks for all pending executions, - // respecting firingIndex overrides and runtime changes - fired = firing = true; - for ( ; queue.length; firingIndex = -1 ) { - memory = queue.shift(); - while ( ++firingIndex < list.length ) { - - // Run callback and check for early termination - if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && - options.stopOnFalse ) { - - // Jump to end and forget the data so .add doesn't re-fire - firingIndex = list.length; - memory = false; - } - } - } - - // Forget the data if we're done with it - if ( !options.memory ) { - memory = false; - } - - firing = false; - - // Clean up if we're done firing for good - if ( locked ) { - - // Keep an empty list if we have data for future add calls - if ( memory ) { - list = []; - - // Otherwise, this object is spent - } else { - list = ""; - } - } - }, - - // Actual Callbacks object - self = { - - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - - // If we have memory from a past run, we should fire after adding - if ( memory && !firing ) { - firingIndex = list.length - 1; - queue.push( memory ); - } - - ( function add( args ) { - jQuery.each( args, function( _, arg ) { - if ( isFunction( arg ) ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && toType( arg ) !== "string" ) { - - // Inspect recursively - add( arg ); - } - } ); - } )( arguments ); - - if ( memory && !firing ) { - fire(); - } - } - return this; - }, - - // Remove a callback from the list - remove: function() { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - - // Handle firing indexes - if ( index <= firingIndex ) { - firingIndex--; - } - } - } ); - return this; - }, - - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? - jQuery.inArray( fn, list ) > -1 : - list.length > 0; - }, - - // Remove all callbacks from the list - empty: function() { - if ( list ) { - list = []; - } - return this; - }, - - // Disable .fire and .add - // Abort any current/pending executions - // Clear all callbacks and values - disable: function() { - locked = queue = []; - list = memory = ""; - return this; - }, - disabled: function() { - return !list; - }, - - // Disable .fire - // Also disable .add unless we have memory (since it would have no effect) - // Abort any pending executions - lock: function() { - locked = queue = []; - if ( !memory && !firing ) { - list = memory = ""; - } - return this; - }, - locked: function() { - return !!locked; - }, - - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( !locked ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - queue.push( args ); - if ( !firing ) { - fire(); - } - } - return this; - }, - - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -function Identity( v ) { - return v; -} -function Thrower( ex ) { - throw ex; -} - -function adoptValue( value, resolve, reject, noValue ) { - var method; - - try { - - // Check for promise aspect first to privilege synchronous behavior - if ( value && isFunction( ( method = value.promise ) ) ) { - method.call( value ).done( resolve ).fail( reject ); - - // Other thenables - } else if ( value && isFunction( ( method = value.then ) ) ) { - method.call( value, resolve, reject ); - - // Other non-thenables - } else { - - // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: - // * false: [ value ].slice( 0 ) => resolve( value ) - // * true: [ value ].slice( 1 ) => resolve() - resolve.apply( undefined, [ value ].slice( noValue ) ); - } - - // For Promises/A+, convert exceptions into rejections - // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in - // Deferred#then to conditionally suppress rejection. - } catch ( value ) { - - // Support: Android 4.0 only - // Strict mode functions invoked without .call/.apply get global-object context - reject.apply( undefined, [ value ] ); - } -} - -jQuery.extend( { - - Deferred: function( func ) { - var tuples = [ - - // action, add listener, callbacks, - // ... .then handlers, argument index, [final state] - [ "notify", "progress", jQuery.Callbacks( "memory" ), - jQuery.Callbacks( "memory" ), 2 ], - [ "resolve", "done", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 0, "resolved" ], - [ "reject", "fail", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 1, "rejected" ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - "catch": function( fn ) { - return promise.then( null, fn ); - }, - - // Keep pipe for back-compat - pipe: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - - return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( _i, tuple ) { - - // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; - - // deferred.progress(function() { bind to newDefer or newDefer.notify }) - // deferred.done(function() { bind to newDefer or newDefer.resolve }) - // deferred.fail(function() { bind to newDefer or newDefer.reject }) - deferred[ tuple[ 1 ] ]( function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && isFunction( returned.promise ) ) { - returned.promise() - .progress( newDefer.notify ) - .done( newDefer.resolve ) - .fail( newDefer.reject ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( - this, - fn ? [ returned ] : arguments - ); - } - } ); - } ); - fns = null; - } ).promise(); - }, - then: function( onFulfilled, onRejected, onProgress ) { - var maxDepth = 0; - function resolve( depth, deferred, handler, special ) { - return function() { - var that = this, - args = arguments, - mightThrow = function() { - var returned, then; - - // Support: Promises/A+ section 2.3.3.3.3 - // https://promisesaplus.com/#point-59 - // Ignore double-resolution attempts - if ( depth < maxDepth ) { - return; - } - - returned = handler.apply( that, args ); - - // Support: Promises/A+ section 2.3.1 - // https://promisesaplus.com/#point-48 - if ( returned === deferred.promise() ) { - throw new TypeError( "Thenable self-resolution" ); - } - - // Support: Promises/A+ sections 2.3.3.1, 3.5 - // https://promisesaplus.com/#point-54 - // https://promisesaplus.com/#point-75 - // Retrieve `then` only once - then = returned && - - // Support: Promises/A+ section 2.3.4 - // https://promisesaplus.com/#point-64 - // Only check objects and functions for thenability - ( typeof returned === "object" || - typeof returned === "function" ) && - returned.then; - - // Handle a returned thenable - if ( isFunction( then ) ) { - - // Special processors (notify) just wait for resolution - if ( special ) { - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ) - ); - - // Normal processors (resolve) also hook into progress - } else { - - // ...and disregard older resolution values - maxDepth++; - - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ), - resolve( maxDepth, deferred, Identity, - deferred.notifyWith ) - ); - } - - // Handle all other returned values - } else { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Identity ) { - that = undefined; - args = [ returned ]; - } - - // Process the value(s) - // Default process is resolve - ( special || deferred.resolveWith )( that, args ); - } - }, - - // Only normal processors (resolve) catch and reject exceptions - process = special ? - mightThrow : - function() { - try { - mightThrow(); - } catch ( e ) { - - if ( jQuery.Deferred.exceptionHook ) { - jQuery.Deferred.exceptionHook( e, - process.error ); - } - - // Support: Promises/A+ section 2.3.3.3.4.1 - // https://promisesaplus.com/#point-61 - // Ignore post-resolution exceptions - if ( depth + 1 >= maxDepth ) { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Thrower ) { - that = undefined; - args = [ e ]; - } - - deferred.rejectWith( that, args ); - } - } - }; - - // Support: Promises/A+ section 2.3.3.3.1 - // https://promisesaplus.com/#point-57 - // Re-resolve promises immediately to dodge false rejection from - // subsequent errors - if ( depth ) { - process(); - } else { - - // Call an optional hook to record the error, in case of exception - // since it's otherwise lost when execution goes async - if ( jQuery.Deferred.getErrorHook ) { - process.error = jQuery.Deferred.getErrorHook(); - - // The deprecated alias of the above. While the name suggests - // returning the stack, not an error instance, jQuery just passes - // it directly to `console.warn` so both will work; an instance - // just better cooperates with source maps. - } else if ( jQuery.Deferred.getStackHook ) { - process.error = jQuery.Deferred.getStackHook(); - } - window.setTimeout( process ); - } - }; - } - - return jQuery.Deferred( function( newDefer ) { - - // progress_handlers.add( ... ) - tuples[ 0 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onProgress ) ? - onProgress : - Identity, - newDefer.notifyWith - ) - ); - - // fulfilled_handlers.add( ... ) - tuples[ 1 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onFulfilled ) ? - onFulfilled : - Identity - ) - ); - - // rejected_handlers.add( ... ) - tuples[ 2 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onRejected ) ? - onRejected : - Thrower - ) - ); - } ).promise(); - }, - - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 5 ]; - - // promise.progress = list.add - // promise.done = list.add - // promise.fail = list.add - promise[ tuple[ 1 ] ] = list.add; - - // Handle state - if ( stateString ) { - list.add( - function() { - - // state = "resolved" (i.e., fulfilled) - // state = "rejected" - state = stateString; - }, - - // rejected_callbacks.disable - // fulfilled_callbacks.disable - tuples[ 3 - i ][ 2 ].disable, - - // rejected_handlers.disable - // fulfilled_handlers.disable - tuples[ 3 - i ][ 3 ].disable, - - // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock, - - // progress_handlers.lock - tuples[ 0 ][ 3 ].lock - ); - } - - // progress_handlers.fire - // fulfilled_handlers.fire - // rejected_handlers.fire - list.add( tuple[ 3 ].fire ); - - // deferred.notify = function() { deferred.notifyWith(...) } - // deferred.resolve = function() { deferred.resolveWith(...) } - // deferred.reject = function() { deferred.rejectWith(...) } - deferred[ tuple[ 0 ] ] = function() { - deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); - return this; - }; - - // deferred.notifyWith = list.fireWith - // deferred.resolveWith = list.fireWith - // deferred.rejectWith = list.fireWith - deferred[ tuple[ 0 ] + "With" ] = list.fireWith; - } ); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( singleValue ) { - var - - // count of uncompleted subordinates - remaining = arguments.length, - - // count of unprocessed arguments - i = remaining, - - // subordinate fulfillment data - resolveContexts = Array( i ), - resolveValues = slice.call( arguments ), - - // the primary Deferred - primary = jQuery.Deferred(), - - // subordinate callback factory - updateFunc = function( i ) { - return function( value ) { - resolveContexts[ i ] = this; - resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( !( --remaining ) ) { - primary.resolveWith( resolveContexts, resolveValues ); - } - }; - }; - - // Single- and empty arguments are adopted like Promise.resolve - if ( remaining <= 1 ) { - adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, - !remaining ); - - // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( primary.state() === "pending" || - isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - - return primary.then(); - } - } - - // Multiple arguments are aggregated like Promise.all array elements - while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); - } - - return primary.promise(); - } -} ); - - -// These usually indicate a programmer mistake during development, -// warn about them ASAP rather than swallowing them by default. -var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; - -// If `jQuery.Deferred.getErrorHook` is defined, `asyncError` is an error -// captured before the async barrier to get the original error cause -// which may otherwise be hidden. -jQuery.Deferred.exceptionHook = function( error, asyncError ) { - - // Support: IE 8 - 9 only - // Console exists when dev tools are open, which can happen at any time - if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { - window.console.warn( "jQuery.Deferred exception: " + error.message, - error.stack, asyncError ); - } -}; - - - - -jQuery.readyException = function( error ) { - window.setTimeout( function() { - throw error; - } ); -}; - - - - -// The deferred used on DOM ready -var readyList = jQuery.Deferred(); - -jQuery.fn.ready = function( fn ) { - - readyList - .then( fn ) - - // Wrap jQuery.readyException in a function so that the lookup - // happens at the time of error handling instead of callback - // registration. - .catch( function( error ) { - jQuery.readyException( error ); - } ); - - return this; -}; - -jQuery.extend( { - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See trac-6781 - readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - } -} ); - -jQuery.ready.then = readyList.then; - -// The ready event handler and self cleanup method -function completed() { - document.removeEventListener( "DOMContentLoaded", completed ); - window.removeEventListener( "load", completed ); - jQuery.ready(); -} - -// Catch cases where $(document).ready() is called -// after the browser event has already occurred. -// Support: IE <=9 - 10 only -// Older IE sometimes signals "interactive" too soon -if ( document.readyState === "complete" || - ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { - - // Handle it asynchronously to allow scripts the opportunity to delay ready - window.setTimeout( jQuery.ready ); - -} else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed ); -} - - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - len = elems.length, - bulk = key == null; - - // Sets many values - if ( toType( key ) === "object" ) { - chainable = true; - for ( i in key ) { - access( elems, fn, i, key[ i ], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, _key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < len; i++ ) { - fn( - elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) - ); - } - } - } - - if ( chainable ) { - return elems; - } - - // Gets - if ( bulk ) { - return fn.call( elems ); - } - - return len ? fn( elems[ 0 ], key ) : emptyGet; -}; - - -// Matches dashed string for camelizing -var rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g; - -// Used by camelCase as callback to replace() -function fcamelCase( _all, letter ) { - return letter.toUpperCase(); -} - -// Convert dashed to camelCase; used by the css and data modules -// Support: IE <=9 - 11, Edge 12 - 15 -// Microsoft forgot to hump their vendor prefix (trac-9572) -function camelCase( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); -} -var acceptData = function( owner ) { - - // Accepts only: - // - Node - // - Node.ELEMENT_NODE - // - Node.DOCUMENT_NODE - // - Object - // - Any - return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); -}; - - - - -function Data() { - this.expando = jQuery.expando + Data.uid++; -} - -Data.uid = 1; - -Data.prototype = { - - cache: function( owner ) { - - // Check if the owner object already has a cache - var value = owner[ this.expando ]; - - // If not, create one - if ( !value ) { - value = {}; - - // We can accept data for non-element nodes in modern browsers, - // but we should not, see trac-8335. - // Always return an empty object. - if ( acceptData( owner ) ) { - - // If it is a node unlikely to be stringify-ed or looped over - // use plain assignment - if ( owner.nodeType ) { - owner[ this.expando ] = value; - - // Otherwise secure it in a non-enumerable property - // configurable must be true to allow the property to be - // deleted when data is removed - } else { - Object.defineProperty( owner, this.expando, { - value: value, - configurable: true - } ); - } - } - } - - return value; - }, - set: function( owner, data, value ) { - var prop, - cache = this.cache( owner ); - - // Handle: [ owner, key, value ] args - // Always use camelCase key (gh-2257) - if ( typeof data === "string" ) { - cache[ camelCase( data ) ] = value; - - // Handle: [ owner, { properties } ] args - } else { - - // Copy the properties one-by-one to the cache object - for ( prop in data ) { - cache[ camelCase( prop ) ] = data[ prop ]; - } - } - return cache; - }, - get: function( owner, key ) { - return key === undefined ? - this.cache( owner ) : - - // Always use camelCase key (gh-2257) - owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; - }, - access: function( owner, key, value ) { - - // In cases where either: - // - // 1. No key was specified - // 2. A string key was specified, but no value provided - // - // Take the "read" path and allow the get method to determine - // which value to return, respectively either: - // - // 1. The entire cache object - // 2. The data stored at the key - // - if ( key === undefined || - ( ( key && typeof key === "string" ) && value === undefined ) ) { - - return this.get( owner, key ); - } - - // When the key is not a string, or both a key and value - // are specified, set or extend (existing objects) with either: - // - // 1. An object of properties - // 2. A key and value - // - this.set( owner, key, value ); - - // Since the "set" path can have two possible entry points - // return the expected data based on which path was taken[*] - return value !== undefined ? value : key; - }, - remove: function( owner, key ) { - var i, - cache = owner[ this.expando ]; - - if ( cache === undefined ) { - return; - } - - if ( key !== undefined ) { - - // Support array or space separated string of keys - if ( Array.isArray( key ) ) { - - // If key is an array of keys... - // We always set camelCase keys, so remove that. - key = key.map( camelCase ); - } else { - key = camelCase( key ); - - // If a key with the spaces exists, use it. - // Otherwise, create an array by matching non-whitespace - key = key in cache ? - [ key ] : - ( key.match( rnothtmlwhite ) || [] ); - } - - i = key.length; - - while ( i-- ) { - delete cache[ key[ i ] ]; - } - } - - // Remove the expando if there's no more data - if ( key === undefined || jQuery.isEmptyObject( cache ) ) { - - // Support: Chrome <=35 - 45 - // Webkit & Blink performance suffers when deleting properties - // from DOM nodes, so set to undefined instead - // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) - if ( owner.nodeType ) { - owner[ this.expando ] = undefined; - } else { - delete owner[ this.expando ]; - } - } - }, - hasData: function( owner ) { - var cache = owner[ this.expando ]; - return cache !== undefined && !jQuery.isEmptyObject( cache ); - } -}; -var dataPriv = new Data(); - -var dataUser = new Data(); - - - -// Implementation Summary -// -// 1. Enforce API surface and semantic compatibility with 1.9.x branch -// 2. Improve the module's maintainability by reducing the storage -// paths to a single mechanism. -// 3. Use the same single mechanism to support "private" and "user" data. -// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) -// 5. Avoid exposing implementation details on user objects (eg. expando properties) -// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 - -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /[A-Z]/g; - -function getData( data ) { - if ( data === "true" ) { - return true; - } - - if ( data === "false" ) { - return false; - } - - if ( data === "null" ) { - return null; - } - - // Only convert to a number if it doesn't change the string - if ( data === +data + "" ) { - return +data; - } - - if ( rbrace.test( data ) ) { - return JSON.parse( data ); - } - - return data; -} - -function dataAttr( elem, key, data ) { - var name; - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = getData( data ); - } catch ( e ) {} - - // Make sure we set the data so it isn't changed later - dataUser.set( elem, key, data ); - } else { - data = undefined; - } - } - return data; -} - -jQuery.extend( { - hasData: function( elem ) { - return dataUser.hasData( elem ) || dataPriv.hasData( elem ); - }, - - data: function( elem, name, data ) { - return dataUser.access( elem, name, data ); - }, - - removeData: function( elem, name ) { - dataUser.remove( elem, name ); - }, - - // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to dataPriv methods, these can be deprecated. - _data: function( elem, name, data ) { - return dataPriv.access( elem, name, data ); - }, - - _removeData: function( elem, name ) { - dataPriv.remove( elem, name ); - } -} ); - -jQuery.fn.extend( { - data: function( key, value ) { - var i, name, data, - elem = this[ 0 ], - attrs = elem && elem.attributes; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = dataUser.get( elem ); - - if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE 11 only - // The attrs elements can be null (trac-14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = camelCase( name.slice( 5 ) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - dataPriv.set( elem, "hasDataAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each( function() { - dataUser.set( this, key ); - } ); - } - - return access( this, function( value ) { - var data; - - // The calling jQuery object (element matches) is not empty - // (and therefore has an element appears at this[ 0 ]) and the - // `value` parameter was not undefined. An empty jQuery object - // will result in `undefined` for elem = this[ 0 ] which will - // throw an exception if an attempt to read a data cache is made. - if ( elem && value === undefined ) { - - // Attempt to get data from the cache - // The key will always be camelCased in Data - data = dataUser.get( elem, key ); - if ( data !== undefined ) { - return data; - } - - // Attempt to "discover" the data in - // HTML5 custom data-* attrs - data = dataAttr( elem, key ); - if ( data !== undefined ) { - return data; - } - - // We tried really hard, but the data doesn't exist. - return; - } - - // Set the data... - this.each( function() { - - // We always store the camelCased key - dataUser.set( this, key, value ); - } ); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each( function() { - dataUser.remove( this, key ); - } ); - } -} ); - - -jQuery.extend( { - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = dataPriv.get( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || Array.isArray( data ) ) { - queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // Clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // Not public - generate a queueHooks object, or return the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { - empty: jQuery.Callbacks( "once memory" ).add( function() { - dataPriv.remove( elem, [ type + "queue", key ] ); - } ) - } ); - } -} ); - -jQuery.fn.extend( { - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[ 0 ], type ); - } - - return data === undefined ? - this : - this.each( function() { - var queue = jQuery.queue( this, type, data ); - - // Ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - } ); - }, - dequeue: function( type ) { - return this.each( function() { - jQuery.dequeue( this, type ); - } ); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -} ); -var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; - -var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); - - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var documentElement = document.documentElement; - - - - var isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ); - }, - composed = { composed: true }; - - // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only - // Check attachment across shadow DOM boundaries when possible (gh-3504) - // Support: iOS 10.0-10.2 only - // Early iOS 10 versions support `attachShadow` but not `getRootNode`, - // leading to errors. We need to check for `getRootNode`. - if ( documentElement.getRootNode ) { - isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ) || - elem.getRootNode( composed ) === elem.ownerDocument; - }; - } -var isHiddenWithinTree = function( elem, el ) { - - // isHiddenWithinTree might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - - // Inline style trumps all - return elem.style.display === "none" || - elem.style.display === "" && - - // Otherwise, check computed style - // Support: Firefox <=43 - 45 - // Disconnected elements can have computed display: none, so first confirm that elem is - // in the document. - isAttached( elem ) && - - jQuery.css( elem, "display" ) === "none"; - }; - - - -function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, scale, - maxIterations = 20, - currentValue = tween ? - function() { - return tween.cur(); - } : - function() { - return jQuery.css( elem, prop, "" ); - }, - initial = currentValue(), - unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - initialInUnit = elem.nodeType && - ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && - rcssNum.exec( jQuery.css( elem, prop ) ); - - if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { - - // Support: Firefox <=54 - // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) - initial = initial / 2; - - // Trust units reported by jQuery.css - unit = unit || initialInUnit[ 3 ]; - - // Iteratively approximate from a nonzero starting point - initialInUnit = +initial || 1; - - while ( maxIterations-- ) { - - // Evaluate and update our best guess (doubling guesses that zero out). - // Finish if the scale equals or crosses 1 (making the old*new product non-positive). - jQuery.style( elem, prop, initialInUnit + unit ); - if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { - maxIterations = 0; - } - initialInUnit = initialInUnit / scale; - - } - - initialInUnit = initialInUnit * 2; - jQuery.style( elem, prop, initialInUnit + unit ); - - // Make sure we update the tween properties later on - valueParts = valueParts || []; - } - - if ( valueParts ) { - initialInUnit = +initialInUnit || +initial || 0; - - // Apply relative offset (+=/-=) if specified - adjusted = valueParts[ 1 ] ? - initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : - +valueParts[ 2 ]; - if ( tween ) { - tween.unit = unit; - tween.start = initialInUnit; - tween.end = adjusted; - } - } - return adjusted; -} - - -var defaultDisplayMap = {}; - -function getDefaultDisplay( elem ) { - var temp, - doc = elem.ownerDocument, - nodeName = elem.nodeName, - display = defaultDisplayMap[ nodeName ]; - - if ( display ) { - return display; - } - - temp = doc.body.appendChild( doc.createElement( nodeName ) ); - display = jQuery.css( temp, "display" ); - - temp.parentNode.removeChild( temp ); - - if ( display === "none" ) { - display = "block"; - } - defaultDisplayMap[ nodeName ] = display; - - return display; -} - -function showHide( elements, show ) { - var display, elem, - values = [], - index = 0, - length = elements.length; - - // Determine new display value for elements that need to change - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - display = elem.style.display; - if ( show ) { - - // Since we force visibility upon cascade-hidden elements, an immediate (and slow) - // check is required in this first loop unless we have a nonempty display value (either - // inline or about-to-be-restored) - if ( display === "none" ) { - values[ index ] = dataPriv.get( elem, "display" ) || null; - if ( !values[ index ] ) { - elem.style.display = ""; - } - } - if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { - values[ index ] = getDefaultDisplay( elem ); - } - } else { - if ( display !== "none" ) { - values[ index ] = "none"; - - // Remember what we're overwriting - dataPriv.set( elem, "display", display ); - } - } - } - - // Set the display of the elements in a second loop to avoid constant reflow - for ( index = 0; index < length; index++ ) { - if ( values[ index ] != null ) { - elements[ index ].style.display = values[ index ]; - } - } - - return elements; -} - -jQuery.fn.extend( { - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each( function() { - if ( isHiddenWithinTree( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - } ); - } -} ); -var rcheckableType = ( /^(?:checkbox|radio)$/i ); - -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); - -var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); - - - -( function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // Support: Android 4.0 - 4.3 only - // Check state lost if the name is set (trac-11217) - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (trac-14901) - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Android <=4.1 only - // Older WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE <=11 only - // Make sure textarea (and checkbox) defaultValue is properly cloned - div.innerHTML = "<textarea>x</textarea>"; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // Support: IE <=9 only - // IE <=9 replaces <option> tags with their contents when inserted outside of - // the select element. - div.innerHTML = "<option></option>"; - support.option = !!div.lastChild; -} )(); - - -// We have to close these tags to support XHTML (trac-13200) -var wrapMap = { - - // XHTML parsers do not magically insert elements in the - // same way that tag soup parsers do. So we cannot shorten - // this by omitting <tbody> or other required elements. - thead: [ 1, "<table>", "</table>" ], - col: [ 2, "<table><colgroup>", "</colgroup></table>" ], - tr: [ 2, "<table><tbody>", "</tbody></table>" ], - td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], - - _default: [ 0, "", "" ] -}; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// Support: IE <=9 only -if ( !support.option ) { - wrapMap.optgroup = wrapMap.option = [ 1, "<select multiple='multiple'>", "</select>" ]; -} - - -function getAll( context, tag ) { - - // Support: IE <=9 - 11 only - // Use typeof to avoid zero-argument method invocation on host objects (trac-15151) - var ret; - - if ( typeof context.getElementsByTagName !== "undefined" ) { - ret = context.getElementsByTagName( tag || "*" ); - - } else if ( typeof context.querySelectorAll !== "undefined" ) { - ret = context.querySelectorAll( tag || "*" ); - - } else { - ret = []; - } - - if ( tag === undefined || tag && nodeName( context, tag ) ) { - return jQuery.merge( [ context ], ret ); - } - - return ret; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - dataPriv.set( - elems[ i ], - "globalEval", - !refElements || dataPriv.get( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/; - -function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, attached, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( toType( elem ) === "object" ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, tmp.childNodes ); - - // Remember the top-level container - tmp = fragment.firstChild; - - // Ensure the created nodes are orphaned (trac-12392) - tmp.textContent = ""; - } - } - } - - // Remove wrapper from fragment - fragment.textContent = ""; - - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { - - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - continue; - } - - attached = isAttached( elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( attached ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - return fragment; -} - - -var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.get( elem ); - - // Only attach events to objects that accept data - if ( !acceptData( elem ) ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Ensure that invalid selectors throw exceptions at attach time - // Evaluate against documentElement in case elem is a non-element node (e.g., document) - if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = Object.create( null ); - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove data and the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - dataPriv.remove( elem, "handle events" ); - } - }, - - dispatch: function( nativeEvent ) { - - var i, j, ret, matched, handleObj, handlerQueue, - args = new Array( arguments.length ), - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( nativeEvent ), - - handlers = ( - dataPriv.get( this, "events" ) || Object.create( null ) - )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - - for ( i = 1; i < arguments.length; i++ ) { - args[ i ] = arguments[ i ]; - } - - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // If the event is namespaced, then each handler is only invoked if it is - // specially universal or its namespaces are a superset of the event's. - if ( !event.rnamespace || handleObj.namespace === false || - event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, handleObj, sel, matchedHandlers, matchedSelectors, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - if ( delegateCount && - - // Support: IE <=9 - // Black-hole SVG <use> instance trees (trac-13180) - cur.nodeType && - - // Support: Firefox <=42 - // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) - // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click - // Support: IE 11 only - // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) - !( event.type === "click" && event.button >= 1 ) ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't check non-elements (trac-13208) - // Don't process clicks on disabled elements (trac-6911, trac-8165, trac-11382, trac-11764) - if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { - matchedHandlers = []; - matchedSelectors = {}; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (trac-13203) - sel = handleObj.selector + " "; - - if ( matchedSelectors[ sel ] === undefined ) { - matchedSelectors[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matchedSelectors[ sel ] ) { - matchedHandlers.push( handleObj ); - } - } - if ( matchedHandlers.length ) { - handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - cur = this; - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - addProp: function( name, hook ) { - Object.defineProperty( jQuery.Event.prototype, name, { - enumerable: true, - configurable: true, - - get: isFunction( hook ) ? - function() { - if ( this.originalEvent ) { - return hook( this.originalEvent ); - } - } : - function() { - if ( this.originalEvent ) { - return this.originalEvent[ name ]; - } - }, - - set: function( value ) { - Object.defineProperty( this, name, { - enumerable: true, - configurable: true, - writable: true, - value: value - } ); - } - } ); - }, - - fix: function( originalEvent ) { - return originalEvent[ jQuery.expando ] ? - originalEvent : - new jQuery.Event( originalEvent ); - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - - // Utilize native event to ensure correct state for checkable inputs - setup: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Claim the first handler - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - // dataPriv.set( el, "click", ... ) - leverageNative( el, "click", true ); - } - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Force setup before triggering a click - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - leverageNative( el, "click" ); - } - - // Return non-false to allow normal event-path propagation - return true; - }, - - // For cross-browser consistency, suppress native .click() on links - // Also prevent it if we're currently inside a leveraged native-event stack - _default: function( event ) { - var target = event.target; - return rcheckableType.test( target.type ) && - target.click && nodeName( target, "input" ) && - dataPriv.get( target, "click" ) || - nodeName( target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - } -}; - -// Ensure the presence of an event listener that handles manually-triggered -// synthetic events by interrupting progress until reinvoked in response to -// *native* events that it fires directly, ensuring that state changes have -// already occurred before other listeners are invoked. -function leverageNative( el, type, isSetup ) { - - // Missing `isSetup` indicates a trigger call, which must force setup through jQuery.event.add - if ( !isSetup ) { - if ( dataPriv.get( el, type ) === undefined ) { - jQuery.event.add( el, type, returnTrue ); - } - return; - } - - // Register the controller as a special universal handler for all event namespaces - dataPriv.set( el, type, false ); - jQuery.event.add( el, type, { - namespace: false, - handler: function( event ) { - var result, - saved = dataPriv.get( this, type ); - - if ( ( event.isTrigger & 1 ) && this[ type ] ) { - - // Interrupt processing of the outer synthetic .trigger()ed event - if ( !saved ) { - - // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. - saved = slice.call( arguments ); - dataPriv.set( this, type, saved ); - - // Trigger the native event and capture its result - this[ type ](); - result = dataPriv.get( this, type ); - dataPriv.set( this, type, false ); - - if ( saved !== result ) { - - // Cancel the outer synthetic event - event.stopImmediatePropagation(); - event.preventDefault(); - - return result; - } - - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering - // the native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. - } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { - event.stopPropagation(); - } - - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved ) { - - // ...and capture the result - dataPriv.set( this, type, jQuery.event.trigger( - saved[ 0 ], - saved.slice( 1 ), - this - ) ); - - // Abort handling of the native event by all jQuery handlers while allowing - // native handlers on the same element to run. On target, this is achieved - // by stopping immediate propagation just on the jQuery event. However, - // the native event is re-wrapped by a jQuery one on each level of the - // propagation so the only way to stop it for jQuery is to stop it for - // everyone via native `stopPropagation()`. This is not a problem for - // focus/blur which don't bubble, but it does also stop click on checkboxes - // and radios. We accept this limitation. - event.stopPropagation(); - event.isImmediatePropagationStopped = returnTrue; - } - } - } ); -} - -jQuery.removeEvent = function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } -}; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? - returnTrue : - returnFalse; - - // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (trac-504, trac-13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - - this.currentTarget = src.currentTarget; - this.relatedTarget = src.relatedTarget; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || Date.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - isSimulated: false, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && !this.isSimulated ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Includes all common event props including KeyEvent and MouseEvent specific props -jQuery.each( { - altKey: true, - bubbles: true, - cancelable: true, - changedTouches: true, - ctrlKey: true, - detail: true, - eventPhase: true, - metaKey: true, - pageX: true, - pageY: true, - shiftKey: true, - view: true, - "char": true, - code: true, - charCode: true, - key: true, - keyCode: true, - button: true, - buttons: true, - clientX: true, - clientY: true, - offsetX: true, - offsetY: true, - pointerId: true, - pointerType: true, - screenX: true, - screenY: true, - targetTouches: true, - toElement: true, - touches: true, - which: true -}, jQuery.event.addProp ); - -jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { - - function focusMappedHandler( nativeEvent ) { - if ( document.documentMode ) { - - // Support: IE 11+ - // Attach a single focusin/focusout handler on the document while someone wants - // focus/blur. This is because the former are synchronous in IE while the latter - // are async. In other browsers, all those handlers are invoked synchronously. - - // `handle` from private data would already wrap the event, but we need - // to change the `type` here. - var handle = dataPriv.get( this, "handle" ), - event = jQuery.event.fix( nativeEvent ); - event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; - event.isSimulated = true; - - // First, handle focusin/focusout - handle( nativeEvent ); - - // ...then, handle focus/blur - // - // focus/blur don't bubble while focusin/focusout do; simulate the former by only - // invoking the handler at the lower level. - if ( event.target === event.currentTarget ) { - - // The setup part calls `leverageNative`, which, in turn, calls - // `jQuery.event.add`, so event handle will already have been set - // by this point. - handle( event ); - } - } else { - - // For non-IE browsers, attach a single capturing handler on the document - // while someone wants focusin/focusout. - jQuery.event.simulate( delegateType, nativeEvent.target, - jQuery.event.fix( nativeEvent ) ); - } - } - - jQuery.event.special[ type ] = { - - // Utilize native event if possible so blur/focus sequence is correct - setup: function() { - - var attaches; - - // Claim the first handler - // dataPriv.set( this, "focus", ... ) - // dataPriv.set( this, "blur", ... ) - leverageNative( this, type, true ); - - if ( document.documentMode ) { - - // Support: IE 9 - 11+ - // We use the same native handler for focusin & focus (and focusout & blur) - // so we need to coordinate setup & teardown parts between those events. - // Use `delegateType` as the key as `type` is already used by `leverageNative`. - attaches = dataPriv.get( this, delegateType ); - if ( !attaches ) { - this.addEventListener( delegateType, focusMappedHandler ); - } - dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 ); - } else { - - // Return false to allow normal processing in the caller - return false; - } - }, - trigger: function() { - - // Force setup before trigger - leverageNative( this, type ); - - // Return non-false to allow normal event-path propagation - return true; - }, - - teardown: function() { - var attaches; - - if ( document.documentMode ) { - attaches = dataPriv.get( this, delegateType ) - 1; - if ( !attaches ) { - this.removeEventListener( delegateType, focusMappedHandler ); - dataPriv.remove( this, delegateType ); - } else { - dataPriv.set( this, delegateType, attaches ); - } - } else { - - // Return false to indicate standard teardown should be applied - return false; - } - }, - - // Suppress native focus or blur if we're currently inside - // a leveraged native-event stack - _default: function( event ) { - return dataPriv.get( event.target, type ); - }, - - delegateType: delegateType - }; - - // Support: Firefox <=44 - // Firefox doesn't have focus(in | out) events - // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 - // - // Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 - // focus(in | out) events fire after focus & blur events, - // which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order - // Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 - // - // Support: IE 9 - 11+ - // To preserve relative focusin/focus & focusout/blur event order guaranteed on the 3.x branch, - // attach a single handler for both events in IE. - jQuery.event.special[ delegateType ] = { - setup: function() { - - // Handle: regular nodes (via `this.ownerDocument`), window - // (via `this.document`) & document (via `this`). - var doc = this.ownerDocument || this.document || this, - dataHolder = document.documentMode ? this : doc, - attaches = dataPriv.get( dataHolder, delegateType ); - - // Support: IE 9 - 11+ - // We use the same native handler for focusin & focus (and focusout & blur) - // so we need to coordinate setup & teardown parts between those events. - // Use `delegateType` as the key as `type` is already used by `leverageNative`. - if ( !attaches ) { - if ( document.documentMode ) { - this.addEventListener( delegateType, focusMappedHandler ); - } else { - doc.addEventListener( type, focusMappedHandler, true ); - } - } - dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this.document || this, - dataHolder = document.documentMode ? this : doc, - attaches = dataPriv.get( dataHolder, delegateType ) - 1; - - if ( !attaches ) { - if ( document.documentMode ) { - this.removeEventListener( delegateType, focusMappedHandler ); - } else { - doc.removeEventListener( type, focusMappedHandler, true ); - } - dataPriv.remove( dataHolder, delegateType ); - } else { - dataPriv.set( dataHolder, delegateType, attaches ); - } - } - }; -} ); - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - } -} ); - - -var - - // Support: IE <=10 - 11, Edge 12 - 13 only - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /<script|<style|<link/i, - - // checked="checked" or checked - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - - rcleanScript = /^\s*<!\[CDATA\[|\]\]>\s*$/g; - -// Prefer a tbody over its parent table for containing new rows -function manipulationTarget( elem, content ) { - if ( nodeName( elem, "table" ) && - nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - - return jQuery( elem ).children( "tbody" )[ 0 ] || elem; - } - - return elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { - elem.type = elem.type.slice( 5 ); - } else { - elem.removeAttribute( "type" ); - } - - return elem; -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.get( src ); - events = pdataOld.events; - - if ( events ) { - dataPriv.remove( dest, "handle events" ); - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = flat( args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - valueIsFunction = isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( valueIsFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( valueIsFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (trac-8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl && !node.noModule ) { - jQuery._evalUrl( node.src, { - nonce: node.nonce || node.getAttribute( "nonce" ) - }, doc ); - } - } else { - - // Unwrap a CDATA section containing script contents. This shouldn't be - // needed as in XML documents they're already not visible when - // inspecting element contents and in HTML documents they have no - // meaning but we're preserving that logic for backwards compatibility. - // This will be removed completely in 4.0. See gh-4904. - DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); - } - } - } - } - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - nodes = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = nodes[ i ] ) != null; i++ ) { - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && isAttached( node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html; - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = isAttached( elem ); - - // Fix IE cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew jQuery#find here for performance reasons: - // https://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - cleanData: function( elems ) { - var data, elem, type, - special = jQuery.event.special, - i = 0; - - for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { - if ( acceptData( elem ) ) { - if ( ( data = elem[ dataPriv.expando ] ) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataPriv.expando ] = undefined; - } - if ( elem[ dataUser.expando ] ) { - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataUser.expando ] = undefined; - } - } - } - } -} ); - -jQuery.fn.extend( { - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each( function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - } ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: Android <=4.0 only, PhantomJS 1 only - // .get() because push.apply(_, arraylike) throws on ancient WebKit - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - -var rcustomProp = /^--/; - - -var getStyles = function( elem ) { - - // Support: IE <=11 only, Firefox <=30 (trac-15098, trac-14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - var view = elem.ownerDocument.defaultView; - - if ( !view || !view.opener ) { - view = window; - } - - return view.getComputedStyle( elem ); - }; - -var swap = function( elem, options, callback ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.call( elem ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - -var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); - - - -( function() { - - // Executing both pixelPosition & boxSizingReliable tests require only one layout - // so they're executed at the same time to save the second computation. - function computeStyleTests() { - - // This is a singleton, we need to execute it only once - if ( !div ) { - return; - } - - container.style.cssText = "position:absolute;left:-11111px;width:60px;" + - "margin-top:1px;padding:0;border:0"; - div.style.cssText = - "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + - "margin:auto;border:1px;padding:1px;" + - "width:60%;top:1%"; - documentElement.appendChild( container ).appendChild( div ); - - var divStyle = window.getComputedStyle( div ); - pixelPositionVal = divStyle.top !== "1%"; - - // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - - // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 - // Some styles come back with percentage values, even though they shouldn't - div.style.right = "60%"; - pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; - - // Support: IE 9 - 11 only - // Detect misreporting of content dimensions for box-sizing:border-box elements - boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; - - // Support: IE 9 only - // Detect overflow:scroll screwiness (gh-3699) - // Support: Chrome <=64 - // Don't get tricked when zoom affects offsetWidth (gh-4029) - div.style.position = "absolute"; - scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; - - documentElement.removeChild( container ); - - // Nullify the div so it wouldn't be stored in the memory and - // it will also be a sign that checks already performed - div = null; - } - - function roundPixelMeasures( measure ) { - return Math.round( parseFloat( measure ) ); - } - - var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, - reliableTrDimensionsVal, reliableMarginLeftVal, - container = document.createElement( "div" ), - div = document.createElement( "div" ); - - // Finish early in limited (non-browser) environments - if ( !div.style ) { - return; - } - - // Support: IE <=9 - 11 only - // Style of cloned element affects source element cloned (trac-8908) - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - jQuery.extend( support, { - boxSizingReliable: function() { - computeStyleTests(); - return boxSizingReliableVal; - }, - pixelBoxStyles: function() { - computeStyleTests(); - return pixelBoxStylesVal; - }, - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, - reliableMarginLeft: function() { - computeStyleTests(); - return reliableMarginLeftVal; - }, - scrollboxSize: function() { - computeStyleTests(); - return scrollboxSizeVal; - }, - - // Support: IE 9 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Behavior in IE 9 is more subtle than in newer versions & it passes - // some versions of this test; make sure not to make it pass there! - // - // Support: Firefox 70+ - // Only Firefox includes border widths - // in computed dimensions. (gh-4529) - reliableTrDimensions: function() { - var table, tr, trChild, trStyle; - if ( reliableTrDimensionsVal == null ) { - table = document.createElement( "table" ); - tr = document.createElement( "tr" ); - trChild = document.createElement( "div" ); - - table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; - tr.style.cssText = "border:1px solid"; - - // Support: Chrome 86+ - // Height set through cssText does not get applied. - // Computed height then comes back as 0. - tr.style.height = "1px"; - trChild.style.height = "9px"; - - // Support: Android 8 Chrome 86+ - // In our bodyBackground.html iframe, - // display for all div elements is set to "inline", - // which causes a problem only in Android 8 Chrome 86. - // Ensuring the div is display: block - // gets around this issue. - trChild.style.display = "block"; - - documentElement - .appendChild( table ) - .appendChild( tr ) - .appendChild( trChild ); - - trStyle = window.getComputedStyle( tr ); - reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + - parseInt( trStyle.borderTopWidth, 10 ) + - parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; - - documentElement.removeChild( table ); - } - return reliableTrDimensionsVal; - } - } ); -} )(); - - -function curCSS( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - isCustomProp = rcustomProp.test( name ), - - // Support: Firefox 51+ - // Retrieving style before computed somehow - // fixes an issue with getting wrong values - // on detached elements - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is needed for: - // .css('filter') (IE 9 only, trac-12537) - // .css('--customProperty) (gh-3144) - if ( computed ) { - - // Support: IE <=9 - 11+ - // IE only supports `"float"` in `getPropertyValue`; in computed styles - // it's only available as `"cssFloat"`. We no longer modify properties - // sent to `.css()` apart from camelCasing, so we need to check both. - // Normally, this would create difference in behavior: if - // `getPropertyValue` returns an empty string, the value returned - // by `.css()` would be `undefined`. This is usually the case for - // disconnected elements. However, in IE even disconnected elements - // with no styles return `"none"` for `getPropertyValue( "float" )` - ret = computed.getPropertyValue( name ) || computed[ name ]; - - if ( isCustomProp && ret ) { - - // Support: Firefox 105+, Chrome <=105+ - // Spec requires trimming whitespace for custom properties (gh-4926). - // Firefox only trims leading whitespace. Chrome just collapses - // both leading & trailing whitespace to a single space. - // - // Fall back to `undefined` if empty string returned. - // This collapses a missing definition with property defined - // and set to an empty string but there's no standard API - // allowing us to differentiate them without a performance penalty - // and returning `undefined` aligns with older jQuery. - // - // rtrimCSS treats U+000D CARRIAGE RETURN and U+000C FORM FEED - // as whitespace while CSS does not, but this is not a problem - // because CSS preprocessing replaces them with U+000A LINE FEED - // (which *is* CSS whitespace) - // https://www.w3.org/TR/css-syntax-3/#input-preprocessing - ret = ret.replace( rtrimCSS, "$1" ) || undefined; - } - - if ( ret === "" && !isAttached( elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Android Browser returns percentage for some values, - // but width seems to be reliably pixels. - // This is against the CSSOM draft spec: - // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret !== undefined ? - - // Support: IE <=9 - 11 only - // IE returns zIndex value as an integer. - ret + "" : - ret; -} - - -function addGetHookIf( conditionFn, hookFn ) { - - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - if ( conditionFn() ) { - - // Hook not needed (or it's not possible to use it due - // to missing dependency), remove it. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - return ( this.get = hookFn ).apply( this, arguments ); - } - }; -} - - -var cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style, - vendorProps = {}; - -// Return a vendor-prefixed property or undefined -function vendorPropName( name ) { - - // Check for vendor prefixed names - var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in emptyStyle ) { - return name; - } - } -} - -// Return a potentially-mapped jQuery.cssProps or vendor prefixed property -function finalPropName( name ) { - var final = jQuery.cssProps[ name ] || vendorProps[ name ]; - - if ( final ) { - return final; - } - if ( name in emptyStyle ) { - return name; - } - return vendorProps[ name ] = vendorPropName( name ) || name; -} - - -var - - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }; - -function setPositiveNumber( _elem, value, subtract ) { - - // Any relative (+/-) values have already been - // normalized at this point - var matches = rcssNum.exec( value ); - return matches ? - - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : - value; -} - -function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { - var i = dimension === "width" ? 1 : 0, - extra = 0, - delta = 0, - marginDelta = 0; - - // Adjustment may not be necessary - if ( box === ( isBorderBox ? "border" : "content" ) ) { - return 0; - } - - for ( ; i < 4; i += 2 ) { - - // Both box models exclude margin - // Count margin delta separately to only add it after scroll gutter adjustment. - // This is needed to make negative margins work with `outerHeight( true )` (gh-3982). - if ( box === "margin" ) { - marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); - } - - // If we get here with a content-box, we're seeking "padding" or "border" or "margin" - if ( !isBorderBox ) { - - // Add padding - delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // For "border" or "margin", add border - if ( box !== "padding" ) { - delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - - // But still keep track of it otherwise - } else { - extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - - // If we get here with a border-box (content + padding + border), we're seeking "content" or - // "padding" or "margin" - } else { - - // For "content", subtract padding - if ( box === "content" ) { - delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // For "content" or "padding", subtract border - if ( box !== "margin" ) { - delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - // Account for positive content-box scroll gutter when requested by providing computedVal - if ( !isBorderBox && computedVal >= 0 ) { - - // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border - // Assuming integer scroll gutter, subtract the rest and round down - delta += Math.max( 0, Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - computedVal - - delta - - extra - - 0.5 - - // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter - // Use an explicit zero to avoid NaN (gh-3964) - ) ) || 0; - } - - return delta + marginDelta; -} - -function getWidthOrHeight( elem, dimension, extra ) { - - // Start with computed style - var styles = getStyles( elem ), - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). - // Fake content-box until we know it's needed to know the true value. - boxSizingNeeded = !support.boxSizingReliable() || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - valueIsBorderBox = isBorderBox, - - val = curCSS( elem, dimension, styles ), - offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - - // Support: Firefox <=54 - // Return a confounding non-pixel value or feign ignorance, as appropriate. - if ( rnumnonpx.test( val ) ) { - if ( !extra ) { - return val; - } - val = "auto"; - } - - - // Support: IE 9 - 11 only - // Use offsetWidth/offsetHeight for when box sizing is unreliable. - // In those cases, the computed value can be trusted to be border-box. - if ( ( !support.boxSizingReliable() && isBorderBox || - - // Support: IE 10 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Interestingly, in some cases IE 9 doesn't suffer from this issue. - !support.reliableTrDimensions() && nodeName( elem, "tr" ) || - - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - val === "auto" || - - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && - - // Make sure the element is visible & connected - elem.getClientRects().length ) { - - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // Where available, offsetWidth/offsetHeight approximate border box dimensions. - // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the - // retrieved value as a content box dimension. - valueIsBorderBox = offsetProp in elem; - if ( valueIsBorderBox ) { - val = elem[ offsetProp ]; - } - } - - // Normalize "" and auto - val = parseFloat( val ) || 0; - - // Adjust for the element's box model - return ( val + - boxModelAdjustment( - elem, - dimension, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles, - - // Provide the current computed size to request scroll gutter calculation (gh-3589) - val - ) - ) + "px"; -} - -jQuery.extend( { - - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - animationIterationCount: true, - aspectRatio: true, - borderImageSlice: true, - columnCount: true, - flexGrow: true, - flexShrink: true, - fontWeight: true, - gridArea: true, - gridColumn: true, - gridColumnEnd: true, - gridColumnStart: true, - gridRow: true, - gridRowEnd: true, - gridRowStart: true, - lineHeight: true, - opacity: true, - order: true, - orphans: true, - scale: true, - widows: true, - zIndex: true, - zoom: true, - - // SVG-related - fillOpacity: true, - floodOpacity: true, - stopOpacity: true, - strokeMiterlimit: true, - strokeOpacity: true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: {}, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ), - style = elem.style; - - // Make sure that we're working with the right name. We don't - // want to query the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Gets hook for the prefixed version, then unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // Convert "+=" or "-=" to relative numbers (trac-7345) - if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { - value = adjustCSS( elem, name, ret ); - - // Fixes bug trac-9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set (trac-7116) - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add the unit (except for certain CSS properties) - // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append - // "px" to a few hardcoded values. - if ( type === "number" && !isCustomProp ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); - } - - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !( "set" in hooks ) || - ( value = hooks.set( elem, value, extra ) ) !== undefined ) { - - if ( isCustomProp ) { - style.setProperty( name, value ); - } else { - style[ name ] = value; - } - } - - } else { - - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && - ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { - - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var val, num, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ); - - // Make sure that we're working with the right name. We don't - // want to modify the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Try prefixed name followed by the unprefixed name - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - // Convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Make numeric if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || isFinite( num ) ? num || 0 : val; - } - - return val; - } -} ); - -jQuery.each( [ "height", "width" ], function( _i, dimension ) { - jQuery.cssHooks[ dimension ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); - } - }, - - set: function( elem, value, extra ) { - var matches, - styles = getStyles( elem ), - - // Only read styles.position if the test has a chance to fail - // to avoid forcing a reflow. - scrollboxSizeBuggy = !support.scrollboxSize() && - styles.position === "absolute", - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) - boxSizingNeeded = scrollboxSizeBuggy || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - subtract = extra ? - boxModelAdjustment( - elem, - dimension, - extra, - isBorderBox, - styles - ) : - 0; - - // Account for unreliable border-box dimensions by comparing offset* to computed and - // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && scrollboxSizeBuggy ) { - subtract -= Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - parseFloat( styles[ dimension ] ) - - boxModelAdjustment( elem, dimension, "border", false, styles ) - - 0.5 - ); - } - - // Convert to pixels if value adjustment is needed - if ( subtract && ( matches = rcssNum.exec( value ) ) && - ( matches[ 3 ] || "px" ) !== "px" ) { - - elem.style[ dimension ] = value; - value = jQuery.css( elem, dimension ); - } - - return setPositiveNumber( elem, value, subtract ); - } - }; -} ); - -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each( { - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // Assumes a single number if not a string - parts = typeof value === "string" ? value.split( " " ) : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( prefix !== "margin" ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -} ); - -jQuery.fn.extend( { - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( Array.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - } -} ); - - -// Based off of the plugin by Clint Helfers, with permission. -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); -}; - - -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); - - input.type = "checkbox"; - - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; - - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; - - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); - } -} ); - -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - - // Attribute names can contain non-HTML whitespace characters - // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 - attrNames = value && value.match( rnothtmlwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); - } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); - } - return name; - } -}; - -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // Use proper attribute retrieval (trac-12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - if ( tabindex ) { - return parseInt( tabindex, 10 ); - } - - if ( - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && - elem.href - ) { - return 0; - } - - return -1; - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - } -} ); - -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -// eslint rule "no-unused-expressions" is disabled for this code -// since it considers such accessions noop -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - - // Strip and collapse whitespace according to HTML spec - // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace - function stripAndCollapse( value ) { - var tokens = value.match( rnothtmlwhite ) || []; - return tokens.join( " " ); - } - - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -function classesToArray( value ) { - if ( Array.isArray( value ) ) { - return value; - } - if ( typeof value === "string" ) { - return value.match( rnothtmlwhite ) || []; - } - return []; -} - -jQuery.fn.extend( { - addClass: function( value ) { - var classNames, cur, curValue, className, i, finalValue; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - classNames = classesToArray( value ); - - if ( classNames.length ) { - return this.each( function() { - curValue = getClass( this ); - cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - if ( cur.indexOf( " " + className + " " ) < 0 ) { - cur += className + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - this.setAttribute( "class", finalValue ); - } - } - } ); - } - - return this; - }, - - removeClass: function( value ) { - var classNames, cur, curValue, className, i, finalValue; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - classNames = classesToArray( value ); - - if ( classNames.length ) { - return this.each( function() { - curValue = getClass( this ); - - // This expression is here for better compressibility (see addClass) - cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - - // Remove *all* instances - while ( cur.indexOf( " " + className + " " ) > -1 ) { - cur = cur.replace( " " + className + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - this.setAttribute( "class", finalValue ); - } - } - } ); - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var classNames, className, i, self, - type = typeof value, - isValidValue = type === "string" || Array.isArray( value ); - - if ( isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); - } - - if ( typeof stateVal === "boolean" && isValidValue ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - classNames = classesToArray( value ); - - return this.each( function() { - if ( isValidValue ) { - - // Toggle individual class names - self = jQuery( this ); - - for ( i = 0; i < classNames.length; i++ ) { - className = classNames[ i ]; - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, valueIsFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - // Handle most common string cases - if ( typeof ret === "string" ) { - return ret.replace( rreturn, "" ); - } - - // Handle cases where value is null/undef or number - return ret == null ? "" : ret; - } - - return; - } - - valueIsFunction = isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( valueIsFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( Array.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - } ); - } -} ); - -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (trac-14686, trac-14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - stripAndCollapse( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, i, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length; - - if ( index < 0 ) { - i = max; - - } else { - i = one ? index : 0; - } - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (trac-2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - /* eslint-disable no-cond-assign */ - - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - - /* eslint-enable no-cond-assign */ - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( Array.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml, parserErrorElem; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) {} - - parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; - if ( !xml || parserErrorElem ) { - jQuery.error( "Invalid XML: " + ( - parserErrorElem ? - jQuery.map( parserErrorElem.childNodes, function( el ) { - return el.textContent; - } ).join( "\n" ) : - data - ) ); - } - return xml; -}; - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - stopPropagationCallback = function( e ) { - e.stopPropagation(); - }; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = lastElement = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (trac-9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724) - if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - lastElement = cur; - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (trac-6170) - if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - - if ( event.isPropagationStopped() ) { - lastElement.addEventListener( type, stopPropagationCallback ); - } - - elem[ type ](); - - if ( event.isPropagationStopped() ) { - lastElement.removeEventListener( type, stopPropagationCallback ); - } - - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -var - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( Array.isArray( obj ) ) { - - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - - // Item is non-scalar (array or object), encode its numeric index. - buildParams( - prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", - v, - traditional, - add - ); - } - } ); - - } else if ( !traditional && toType( obj ) === "object" ) { - - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, valueOrFunction ) { - - // If value is a function, invoke it and use its return value - var value = isFunction( valueOrFunction ) ? - valueOrFunction() : - valueOrFunction; - - s[ s.length ] = encodeURIComponent( key ) + "=" + - encodeURIComponent( value == null ? "" : value ); - }; - - if ( a == null ) { - return ""; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - } ); - - } else { - - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ); -}; - -jQuery.fn.extend( { - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map( function() { - - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - } ).filter( function() { - var type = this.type; - - // Use .is( ":disabled" ) so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - } ).map( function( _i, elem ) { - var val = jQuery( this ).val(); - - if ( val == null ) { - return null; - } - - if ( Array.isArray( val ) ) { - return jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ); - } - - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ).get(); - } -} ); - - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var htmlIsFunction = isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; - } -} ); - - -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); -}; - - - - -// Support: Safari 8 only -// In Safari 8 documents created via document.implementation.createHTMLDocument -// collapse sibling forms: the second one becomes a child of the first one. -// Because of that, this security measure has to be disabled in Safari 8. -// https://bugs.webkit.org/show_bug.cgi?id=137337 -support.createHTMLDocument = ( function() { - var body = document.implementation.createHTMLDocument( "" ).body; - body.innerHTML = "<form></form><form></form>"; - return body.childNodes.length === 2; -} )(); - - -// Argument "data" should be string of html -// context (optional): If specified, the fragment will be created in this context, -// defaults to document -// keepScripts (optional): If true, will include scripts passed in the html string -jQuery.parseHTML = function( data, context, keepScripts ) { - if ( typeof data !== "string" ) { - return []; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - - var base, parsed, scripts; - - if ( !context ) { - - // Stop scripts or inline event handlers from being executed immediately - // by using document.implementation - if ( support.createHTMLDocument ) { - context = document.implementation.createHTMLDocument( "" ); - - // Set the base href for the created document - // so any parsed elements with URLs - // are based on the document's URL (gh-2965) - base = context.createElement( "base" ); - base.href = document.location.href; - context.head.appendChild( base ); - } else { - context = document; - } - } - - parsed = rsingleTag.exec( data ); - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[ 1 ] ) ]; - } - - parsed = buildFragment( [ data ], context, scripts ); - - if ( scripts && scripts.length ) { - jQuery( scripts ).remove(); - } - - return jQuery.merge( [], parsed.childNodes ); -}; - - -jQuery.offset = { - setOffset: function( elem, options, i ) { - var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, - position = jQuery.css( elem, "position" ), - curElem = jQuery( elem ), - props = {}; - - // Set position first, in-case top/left are set even on static elem - if ( position === "static" ) { - elem.style.position = "relative"; - } - - curOffset = curElem.offset(); - curCSSTop = jQuery.css( elem, "top" ); - curCSSLeft = jQuery.css( elem, "left" ); - calculatePosition = ( position === "absolute" || position === "fixed" ) && - ( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1; - - // Need to be able to calculate position if either - // top or left is auto and position is either absolute or fixed - if ( calculatePosition ) { - curPosition = curElem.position(); - curTop = curPosition.top; - curLeft = curPosition.left; - - } else { - curTop = parseFloat( curCSSTop ) || 0; - curLeft = parseFloat( curCSSLeft ) || 0; - } - - if ( isFunction( options ) ) { - - // Use jQuery.extend here to allow modification of coordinates argument (gh-1848) - options = options.call( elem, i, jQuery.extend( {}, curOffset ) ); - } - - if ( options.top != null ) { - props.top = ( options.top - curOffset.top ) + curTop; - } - if ( options.left != null ) { - props.left = ( options.left - curOffset.left ) + curLeft; - } - - if ( "using" in options ) { - options.using.call( elem, props ); - - } else { - curElem.css( props ); - } - } -}; - -jQuery.fn.extend( { - - // offset() relates an element's border box to the document origin - offset: function( options ) { - - // Preserve chaining for setter - if ( arguments.length ) { - return options === undefined ? - this : - this.each( function( i ) { - jQuery.offset.setOffset( this, options, i ); - } ); - } - - var rect, win, - elem = this[ 0 ]; - - if ( !elem ) { - return; - } - - // Return zeros for disconnected and hidden (display: none) elements (gh-2310) - // Support: IE <=11 only - // Running getBoundingClientRect on a - // disconnected node in IE throws an error - if ( !elem.getClientRects().length ) { - return { top: 0, left: 0 }; - } - - // Get document-relative position by adding viewport scroll to viewport-relative gBCR - rect = elem.getBoundingClientRect(); - win = elem.ownerDocument.defaultView; - return { - top: rect.top + win.pageYOffset, - left: rect.left + win.pageXOffset - }; - }, - - // position() relates an element's margin box to its offset parent's padding box - // This corresponds to the behavior of CSS absolute positioning - position: function() { - if ( !this[ 0 ] ) { - return; - } - - var offsetParent, offset, doc, - elem = this[ 0 ], - parentOffset = { top: 0, left: 0 }; - - // position:fixed elements are offset from the viewport, which itself always has zero offset - if ( jQuery.css( elem, "position" ) === "fixed" ) { - - // Assume position:fixed implies availability of getBoundingClientRect - offset = elem.getBoundingClientRect(); - - } else { - offset = this.offset(); - - // Account for the *real* offset parent, which can be the document or its root element - // when a statically positioned element is identified - doc = elem.ownerDocument; - offsetParent = elem.offsetParent || doc.documentElement; - while ( offsetParent && - ( offsetParent === doc.body || offsetParent === doc.documentElement ) && - jQuery.css( offsetParent, "position" ) === "static" ) { - - offsetParent = offsetParent.parentNode; - } - if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) { - - // Incorporate borders into its offset, since they are outside its content origin - parentOffset = jQuery( offsetParent ).offset(); - parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true ); - parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true ); - } - } - - // Subtract parent offsets and element margins - return { - top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), - left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true ) - }; - }, - - // This method will return documentElement in the following cases: - // 1) For the element inside the iframe without offsetParent, this method will return - // documentElement of the parent window - // 2) For the hidden or detached element - // 3) For body or html element, i.e. in case of the html node - it will return itself - // - // but those exceptions were never presented as a real life use-cases - // and might be considered as more preferable results. - // - // This logic, however, is not guaranteed and can change at any point in the future - offsetParent: function() { - return this.map( function() { - var offsetParent = this.offsetParent; - - while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) { - offsetParent = offsetParent.offsetParent; - } - - return offsetParent || documentElement; - } ); - } -} ); - -// Create scrollLeft and scrollTop methods -jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { - var top = "pageYOffset" === prop; - - jQuery.fn[ method ] = function( val ) { - return access( this, function( elem, method, val ) { - - // Coalesce documents and windows - var win; - if ( isWindow( elem ) ) { - win = elem; - } else if ( elem.nodeType === 9 ) { - win = elem.defaultView; - } - - if ( val === undefined ) { - return win ? win[ prop ] : elem[ method ]; - } - - if ( win ) { - win.scrollTo( - !top ? val : win.pageXOffset, - top ? val : win.pageYOffset - ); - - } else { - elem[ method ] = val; - } - }, method, val, arguments.length ); - }; -} ); - -// Support: Safari <=7 - 9.1, Chrome <=37 - 49 -// Add the top/left cssHooks using jQuery.fn.position -// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 -// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347 -// getComputedStyle returns percent when specified for top/left/bottom/right; -// rather than make the css module depend on the offset module, just check for it here -jQuery.each( [ "top", "left" ], function( _i, prop ) { - jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, - function( elem, computed ) { - if ( computed ) { - computed = curCSS( elem, prop ); - - // If curCSS returns percentage, fallback to offset - return rnumnonpx.test( computed ) ? - jQuery( elem ).position()[ prop ] + "px" : - computed; - } - } - ); -} ); - - -// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods -jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { - jQuery.each( { - padding: "inner" + name, - content: type, - "": "outer" + name - }, function( defaultExtra, funcName ) { - - // Margin is only for outerHeight, outerWidth - jQuery.fn[ funcName ] = function( margin, value ) { - var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), - extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); - - return access( this, function( elem, type, value ) { - var doc; - - if ( isWindow( elem ) ) { - - // $( window ).outerWidth/Height return w/h including scrollbars (gh-1729) - return funcName.indexOf( "outer" ) === 0 ? - elem[ "inner" + name ] : - elem.document.documentElement[ "client" + name ]; - } - - // Get document width or height - if ( elem.nodeType === 9 ) { - doc = elem.documentElement; - - // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], - // whichever is greatest - return Math.max( - elem.body[ "scroll" + name ], doc[ "scroll" + name ], - elem.body[ "offset" + name ], doc[ "offset" + name ], - doc[ "client" + name ] - ); - } - - return value === undefined ? - - // Get width or height on the element, requesting but not forcing parseFloat - jQuery.css( elem, type, extra ) : - - // Set width or height on the element - jQuery.style( elem, type, value, extra ); - }, type, chainable ? margin : undefined, chainable ); - }; - } ); -} ); - - -jQuery.fn.extend( { - - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, - - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length === 1 ? - this.off( selector, "**" ) : - this.off( types, selector || "**", fn ); - }, - - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -} ); - -jQuery.each( - ( "blur focus focusin focusout resize scroll click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup contextmenu" ).split( " " ), - function( _i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; - } -); - - - - -// Support: Android <=4.0 only -// Make sure we trim BOM and NBSP -// Require that the "whitespace run" starts from a non-whitespace -// to avoid O(N^2) behavior when the engine would try matching "\s+$" at each space position. -var rtrim = /^[\s\uFEFF\xA0]+|([^\s\uFEFF\xA0])[\s\uFEFF\xA0]+$/g; - -// Bind a function to a context, optionally partially applying any -// arguments. -// jQuery.proxy is deprecated to promote standards (specifically Function#bind) -// However, it is not slated for removal any time soon -jQuery.proxy = function( fn, context ) { - var tmp, args, proxy; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; -}; - -jQuery.holdReady = function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } -}; -jQuery.isArray = Array.isArray; -jQuery.parseJSON = JSON.parse; -jQuery.nodeName = nodeName; -jQuery.isFunction = isFunction; -jQuery.isWindow = isWindow; -jQuery.camelCase = camelCase; -jQuery.type = toType; - -jQuery.now = Date.now; - -jQuery.isNumeric = function( obj ) { - - // As of jQuery 3.0, isNumeric is limited to - // strings and numbers (primitives or objects) - // that can be coerced to finite numbers (gh-2662) - var type = jQuery.type( obj ); - return ( type === "number" || type === "string" ) && - - // parseFloat NaNs numeric-cast false positives ("") - // ...but misinterprets leading-number strings, particularly hex literals ("0x...") - // subtraction forces infinities to NaN - !isNaN( obj - parseFloat( obj ) ); -}; - -jQuery.trim = function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "$1" ); -}; - - - -// Register as a named AMD module, since jQuery can be concatenated with other -// files that may use define, but not via a proper concatenation script that -// understands anonymous AMD modules. A named AMD is safest and most robust -// way to register. Lowercase jquery is used because AMD module names are -// derived from file names, and jQuery is normally delivered in a lowercase -// file name. Do this after creating the global so that if an AMD module wants -// to call noConflict to hide this version of jQuery, it will work. - -// Note that for maximum portability, libraries that are not jQuery should -// declare themselves as anonymous modules, and avoid setting a global if an -// AMD loader is present. jQuery is a special case. For more information, see -// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon - -if ( typeof define === "function" && define.amd ) { - define( "jquery", [], function() { - return jQuery; - } ); -} - - - - -var - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$; - -jQuery.noConflict = function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; -}; - -// Expose jQuery and $ identifiers, even in AMD -// (trac-7102#comment:10, https://github.com/jquery/jquery/pull/557) -// and CommonJS for browser emulators (trac-13566) -if ( typeof noGlobal === "undefined" ) { - window.jQuery = window.$ = jQuery; -} - - - - -return jQuery; -} ); diff --git a/src/static/scripts/jquery-3.7.1.slim.js b/src/static/scripts/jquery-3.7.1.slim.js @@ -0,0 +1,8617 @@ +/*! + * jQuery JavaScript Library v3.7.1 -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/animatedSelector,-effects/Tween + * https://jquery.com/ + * + * Copyright OpenJS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2023-08-28T13:37Z + */ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket trac-14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 +// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode +// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common +// enough that all such attempts are guarded in a try block. +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML <object> elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 + // Plus for old WebKit, typeof returns "function" for HTML collections + // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; + + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + +var document = window.document; + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} +/* global Symbol */ +// Defining this global in .eslintrc.json would create a danger of using the global +// unguarded in another place, it seems safer to define global only for this module + + + +var version = "3.7.1 -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/animatedSelector,-effects/Tween", + + rhtmlSuffix = /HTML$/i, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + + // Return all the elements in a clean array + if ( num == null ) { + return slice.call( this ); + } + + // Return just the one element from the set + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + copy = options[ name ]; + + // Prevent Object.prototype pollution + // Prevent never-ending loop + if ( name === "__proto__" || target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + // Detect obvious negatives + // Use toString instead of jQuery.type to catch host objects + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + // Objects with no prototype (e.g., `Object.create( null )`) are plain + if ( !proto ) { + return true; + } + + // Objects with prototype are plain iff they were constructed by a global Object function + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + // Evaluates a script in a provided context; falls back to the global one + // if not specified. + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + + // Retrieve the text value of an array of DOM nodes + text: function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + // If no nodeType, this is expected to be an array + while ( ( node = elem[ i++ ] ) ) { + + // Do not traverse comment nodes + ret += jQuery.text( node ); + } + } + if ( nodeType === 1 || nodeType === 11 ) { + return elem.textContent; + } + if ( nodeType === 9 ) { + return elem.documentElement.textContent; + } + if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + // Do not include comment or processing instruction nodes + + return ret; + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + isXMLDoc: function( elem ) { + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; + + // Assume HTML when documentElement doesn't yet exist, such as inside + // document fragments. + return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); + }, + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return flat( ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function isArrayLike( obj ) { + + // Support: real iOS 8.2 only (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} + + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +} +var pop = arr.pop; + + +var sort = arr.sort; + + +var splice = arr.splice; + + +var whitespace = "[\\x20\\t\\r\\n\\f]"; + + +var rtrimCSS = new RegExp( + "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", + "g" +); + + + + +// Note: an element does not contain itself +jQuery.contains = function( a, b ) { + var bup = b && b.parentNode; + + return a === bup || !!( bup && bup.nodeType === 1 && ( + + // Support: IE 9 - 11+ + // IE doesn't have `contains` on SVG. + a.contains ? + a.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); +}; + + + + +// CSS string/identifier serialization +// https://drafts.csswg.org/cssom/#common-serializing-idioms +var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + +function fcssescape( ch, asCodePoint ) { + if ( asCodePoint ) { + + // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER + if ( ch === "\0" ) { + return "\uFFFD"; + } + + // Control characters and (dependent upon position) numbers get escaped as code points + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + // Other potentially-special ASCII characters get backslash-escaped + return "\\" + ch; +} + +jQuery.escapeSelector = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + + + + +var preferredDoc = document, + pushNative = push; + +( function() { + +var i, + Expr, + outermostContext, + sortInput, + hasDuplicate, + push = pushNative, + + // Local document vars + document, + documentElement, + documentIsHTML, + rbuggyQSA, + matches, + + // Instance-specific data + expando = jQuery.expando, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + + "loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + // Attribute selectors: https://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + + whitespace + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + ID: new RegExp( "^#(" + identifier + ")" ), + CLASS: new RegExp( "^\\.(" + identifier + ")" ), + TAG: new RegExp( "^(" + identifier + "|[*])" ), + ATTR: new RegExp( "^" + attributes ), + PSEUDO: new RegExp( "^" + pseudos ), + CHILD: new RegExp( + "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + bool: new RegExp( "^(?:" + booleans + ")$", "i" ), + + // For use in libraries implementing .is() + // We use this for POS matching in `select` + needsContext: new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + // CSS escapes + // https://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + if ( nonHex ) { + + // Strip the backslash prefix from a non-hex escape sequence + return nonHex; + } + + // Replace a hexadecimal escape sequence with the encoded Unicode code point + // Support: IE <=11+ + // For values outside the Basic Multilingual Plane (BMP), manually construct a + // surrogate pair + return high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes; see `setDocument`. + // Support: IE 9 - 11+, Edge 12 - 18+ + // Removing the function wrapper causes a "Permission Denied" + // error in IE/Edge. + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && nodeName( elem, "fieldset" ); + }, + { dir: "parentNode", next: "legend" } + ); + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + // Support: Android <=4.0 + // Detect silently failing push.apply + // eslint-disable-next-line no-unused-expressions + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { + apply: function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + }, + call: function( target ) { + pushNative.apply( target, slice.call( arguments, 1 ) ); + } + }; +} + +function find( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + // ID selector + if ( ( m = match[ 1 ] ) ) { + + // Document context + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + // Support: IE 9 only + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + push.call( results, elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE 9 only + // getElementById can match elements by name instead of ID + if ( newContext && ( elem = newContext.getElementById( m ) ) && + find.contains( context, elem ) && + elem.id === m ) { + + push.call( results, elem ); + return results; + } + } + + // Type selector + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // The technique has to be used as well when a leading combinator is used + // as such selectors are not recognized by querySelectorAll. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && + ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + // We can use :scope instead of the ID hack if the browser + // supports it & if we're not changing the context. + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when + // strict-comparing two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( newContext != context || !support.scope ) { + + // Capture the context ID, setting it first if necessary + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = jQuery.escapeSelector( nid ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + // All others + return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + + // Use (key + " ") to avoid collision with native prototype properties + // (see https://github.com/jquery/sizzle/issues/157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +/** + * Mark a function for special use by jQuery selector module + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created element and returns a boolean result + */ +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + // Remove from its parent by default + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + // release memory in IE + el = null; + } +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + return nodeName( elem, "input" ) && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + return ( nodeName( elem, "input" ) || nodeName( elem, "button" ) ) && + elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for :enabled/:disabled + * @param {Boolean} disabled true for :disabled; false for :enabled + */ +function createDisabledPseudo( disabled ) { + + // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable + return function( elem ) { + + // Only certain elements can match :enabled or :disabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled + // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled + if ( "form" in elem ) { + + // Check for inherited disabledness on relevant non-disabled elements: + // * listed form-associated elements in a disabled fieldset + // https://html.spec.whatwg.org/multipage/forms.html#category-listed + // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled + // * option elements in a disabled optgroup + // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled + // All such elements have a "form" property. + if ( elem.parentNode && elem.disabled === false ) { + + // Option elements defer to a parent optgroup if present + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + // Support: IE 6 - 11+ + // Use the isDisabled shortcut property to check for disabled fieldset ancestors + return elem.isDisabled === disabled || + + // Where there is no isDisabled, check manually + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + // Try to winnow out elements that can't be disabled before trusting the disabled property. + // Some victims get caught in our net (label, legend, menu, track), but it shouldn't + // even exist on them, let alone have a boolean value. + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + // Remaining elements are neither :enabled nor :disabled + return false; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +/** + * Checks a node for validity as a jQuery selector context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [node] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +function setDocument( node ) { + var subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + documentElement = document.documentElement; + documentIsHTML = !jQuery.isXMLDoc( document ); + + // Support: iOS 7 only, IE 9 - 11+ + // Older browsers didn't support unprefixed `matches`. + matches = documentElement.matches || + documentElement.webkitMatchesSelector || + documentElement.msMatchesSelector; + + // Support: IE 9 - 11+, Edge 12 - 18+ + // Accessing iframe documents after unload throws "permission denied" errors + // (see trac-13936). + // Limit the fix to IE & Edge Legacy; despite Edge 15+ implementing `matches`, + // all IE 9+ and Edge Legacy versions implement `msMatchesSelector` as well. + if ( documentElement.msMatchesSelector && + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + // Support: IE 9 - 11+, Edge 12 - 18+ + subWindow.addEventListener( "unload", unloadHandler ); + } + + // Support: IE <10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programmatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert( function( el ) { + documentElement.appendChild( el ).id = jQuery.expando; + return !document.getElementsByName || + !document.getElementsByName( jQuery.expando ).length; + } ); + + // Support: IE 9 only + // Check to see if it's possible to do matchesSelector + // on a disconnected node. + support.disconnectedMatch = assert( function( el ) { + return matches.call( el, "*" ); + } ); + + // Support: IE 9 - 11+, Edge 12 - 18+ + // IE/Edge don't support the :scope pseudo-class. + support.scope = assert( function() { + return document.querySelectorAll( ":scope" ); + } ); + + // Support: Chrome 105 - 111 only, Safari 15.4 - 16.3 only + // Make sure the `:has()` argument is parsed unforgivingly. + // We include `*` in the test to detect buggy implementations that are + // _selectively_ forgiving (specifically when the list includes at least + // one valid selector). + // Note that we treat complete lack of support for `:has()` as if it were + // spec-compliant support, which is fine because use of `:has()` in such + // environments will fail in the qSA path and fall back to jQuery traversal + // anyway. + support.cssHas = assert( function() { + try { + document.querySelector( ":has(*,:jqfake)" ); + return false; + } catch ( e ) { + return true; + } + } ); + + // ID filter and find + if ( support.getById ) { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + // Support: IE 6 - 7 only + // getElementById is not reliable as a find shortcut + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + // Verify the id attribute + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + // Fall back on getElementsByName + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + // Tag + Expr.find.TAG = function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else { + return context.querySelectorAll( tag ); + } + }; + + // Class + Expr.find.CLASS = function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + rbuggyQSA = []; + + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert( function( el ) { + + var input; + + documentElement.appendChild( el ).innerHTML = + "<a id='" + expando + "' href='' disabled='disabled'></a>" + + "<select id='" + expando + "-\r\\' disabled='disabled'>" + + "<option selected=''></option></select>"; + + // Support: iOS <=7 - 8 only + // Boolean attributes and "value" are not treated correctly in some XML documents + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: iOS <=7 - 8 only + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + // Support: iOS 8 only + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibling-combinator selector` fails + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ + // In some of the document kinds, these selectors wouldn't work natively. + // This is probably OK but for backwards compatibility we want to maintain + // handling them through jQuery traversal in jQuery 3.x. + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE 9 - 11+ + // IE's :disabled selector does not pick up the children of disabled fieldsets + // Support: Chrome <=105+, Firefox <=104+, Safari <=15.4+ + // In some of the document kinds, these selectors wouldn't work natively. + // This is probably OK but for backwards compatibility we want to maintain + // handling them through jQuery traversal in jQuery 3.x. + documentElement.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Support: IE 11+, Edge 15 - 18+ + // IE 11/Edge don't find elements on a `[name='']` query in some cases. + // Adding a temporary attribute to the document before the selection works + // around the issue. + // Interestingly, IE 10 & older don't seem to have the issue. + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + } ); + + if ( !support.cssHas ) { + + // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+ + // Our regular `try-catch` mechanism fails to detect natively-unsupported + // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`) + // in browsers that parse the `:has()` argument as a forgiving selector list. + // https://drafts.csswg.org/selectors/#relational now requires the argument + // to be parsed unforgivingly, but browsers have not yet fully adjusted. + rbuggyQSA.push( ":has" ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + // Choose the first element that is related to our preferred document + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( a === document || a.ownerDocument == preferredDoc && + find.contains( preferredDoc, a ) ) { + return -1; + } + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( b === document || b.ownerDocument == preferredDoc && + find.contains( preferredDoc, b ) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + }; + + return document; +} + +find.matches = function( expr, elements ) { + return find( expr, null, null, elements ); +}; + +find.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return find( expr, document, null, [ elem ] ).length > 0; +}; + +find.contains = function( context, elem ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return jQuery.contains( context, elem ); +}; + + +find.attr = function( elem, name ) { + + // Set document vars if needed + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + // Don't get fooled by Object.prototype properties (see trac-13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + if ( val !== undefined ) { + return val; + } + + return elem.getAttribute( name ); +}; + +find.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +jQuery.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + // + // Support: Android <=4.0+ + // Testing for detecting duplicates is unpredictable so instead assume we can't + // depend on duplicate detection in all browsers without a stable sort. + hasDuplicate = !support.sortStable; + sortInput = !support.sortStable && slice.call( results, 0 ); + sort.call( results, sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + splice.call( results, duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +jQuery.fn.uniqueSort = function() { + return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); +}; + +Expr = jQuery.expr = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + ATTR: function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) + .replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + CHILD: function( match ) { + + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + // nth-* requires argument + if ( !match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) + ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + // other types prohibit arguments + } else if ( match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + return match; + }, + + PSEUDO: function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr.CHILD.test( match[ 0 ] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + + // Get excess from tokenize (recursively) + ( excess = tokenize( unquoted, true ) ) && + + // advance to the next closing parenthesis + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + // excess is a negative index + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + TAG: function( nodeNameSelector ) { + var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return nodeName( elem, expectedNodeName ); + }; + }, + + CLASS: function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + ")" + className + + "(" + whitespace + "|$)" ) ) && + classCache( className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + ATTR: function( name, operator, check ) { + return function( elem ) { + var result = find.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + if ( operator === "=" ) { + return result === check; + } + if ( operator === "!=" ) { + return result !== check; + } + if ( operator === "^=" ) { + return check && result.indexOf( check ) === 0; + } + if ( operator === "*=" ) { + return check && result.indexOf( check ) > -1; + } + if ( operator === "$=" ) { + return check && result.slice( -check.length ) === check; + } + if ( operator === "~=" ) { + return ( " " + result.replace( rwhitespace, " " ) + " " ) + .indexOf( check ) > -1; + } + if ( operator === "|=" ) { + return result === check || result.slice( 0, check.length + 1 ) === check + "-"; + } + + return false; + }; + }, + + CHILD: function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) { + + return false; + } + } + + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || ( parent[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + // Use previously-cached element index if available + if ( useCache ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + + // Use the same loop as above to seek `elem` from the start + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + outerCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + PSEUDO: function( pseudo, argument ) { + + // pseudo-class names are case-insensitive + // https://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + find.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as jQuery does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + // Potentially complex pseudos + not: markFunction( function( selector ) { + + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrimCSS, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + // Don't keep the element + // (see https://github.com/jquery/sizzle/issues/299) + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + has: markFunction( function( selector ) { + return function( elem ) { + return find( selector, elem ).length > 0; + }; + } ), + + contains: markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; + }; + } ), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // https://www.w3.org/TR/selectors/#lang-pseudo + lang: markFunction( function( lang ) { + + // lang value must be a valid identifier + if ( !ridentifier.test( lang || "" ) ) { + find.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + // Miscellaneous + target: function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + root: function( elem ) { + return elem === documentElement; + }, + + focus: function( elem ) { + return elem === safeActiveElement() && + document.hasFocus() && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + // Boolean properties + enabled: createDisabledPseudo( false ), + disabled: createDisabledPseudo( true ), + + checked: function( elem ) { + + // In CSS3, :checked should return both checked and selected elements + // https://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + return ( nodeName( elem, "input" ) && !!elem.checked ) || + ( nodeName( elem, "option" ) && !!elem.selected ); + }, + + selected: function( elem ) { + + // Support: IE <=11+ + // Accessing the selectedIndex property + // forces the browser to treat the default option as + // selected when in an optgroup. + if ( elem.parentNode ) { + // eslint-disable-next-line no-unused-expressions + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + empty: function( elem ) { + + // https://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + parent: function( elem ) { + return !Expr.pseudos.empty( elem ); + }, + + // Element/input types + header: function( elem ) { + return rheader.test( elem.nodeName ); + }, + + input: function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + button: function( elem ) { + return nodeName( elem, "input" ) && elem.type === "button" || + nodeName( elem, "button" ); + }, + + text: function( elem ) { + var attr; + return nodeName( elem, "input" ) && elem.type === "text" && + + // Support: IE <10 only + // New HTML5 attribute values (e.g., "search") appear + // with elem.type === "text" + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + first: createPositionalPseudo( function() { + return [ 0 ]; + } ), + + last: createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + eq: createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + even: createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + odd: createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + lt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i; + + if ( argument < 0 ) { + i = argument + length; + } else if ( argument > length ) { + i = length; + } else { + i = argument; + } + + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + gt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos.nth = Expr.pseudos.eq; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + // Don't consume trailing commas as valid + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + // Combinators + if ( ( match = rleadingCombinator.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + // Cast descendant combinators to space + type: match[ 0 ].replace( rtrimCSS, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + if ( parseOnly ) { + return soFar.length; + } + + return soFar ? + find.error( selector ) : + + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +} + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + if ( skip && nodeName( elem, skip ) ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = outerCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + // Reuse newcache so results back-propagate to previous elements + outerCache[ key ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + find( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, matcherOut, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || + multipleContexts( selector || "*", + context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems; + + if ( matcher ) { + + // If we have a postFinder, or filtered seed, or non-seed postFilter + // or preexisting results, + matcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results; + + // Find primary matches + matcher( matcherIn, matcherOut, context, xml ); + } else { + matcherOut = matcherIn; + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + // Restore matcherIn since elem is not yet a final match + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + // Avoid hanging onto element + // (see https://github.com/jquery/sizzle/issues/299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrimCSS, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find.TAG( "*", outermost ), + + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + outermostContext = context == document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: iOS <=7 - 9 only + // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching + // elements by id. (see trac-14142) + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + // Support: IE 11+, Edge 17 - 18+ + // IE/Edge sometimes throw a "Permission denied" error when strict-comparing + // two documents; shallow comparisons work. + // eslint-disable-next-line eqeqeq + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + push.call( results, elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + + // They will have gone through all possible matchers + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + jQuery.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +function compile( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +} + +/** + * A low-level selection function that works with jQuery's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with jQuery selector compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find.ID( + token.matches[ 0 ].replace( runescape, funescape ), + context + ) || [] )[ 0 ]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr.needsContext.test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + // Abort if we hit a combinator + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + // Search, expanding context for leading sibling combinators + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && + testContext( context.parentNode ) || context + ) ) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +} + +// One-time assignments + +// Support: Android <=4.0 - 4.1+ +// Sort stability +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +// Initialize against the default document +setDocument(); + +// Support: Android <=4.0 - 4.1+ +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert( function( el ) { + + // Should return 1, but returns 4 (following) + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +jQuery.find = find; + +// Deprecated +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.unique = jQuery.uniqueSort; + +// These have always been private, but they used to be documented as part of +// Sizzle so let's maintain them for now for backwards compatibility purposes. +find.compile = compile; +find.select = select; +find.setDocument = setDocument; +find.tokenize = tokenize; + +find.escape = jQuery.escapeSelector; +find.getText = jQuery.text; +find.isXML = jQuery.isXMLDoc; +find.selectors = jQuery.expr; +find.support = jQuery.support; +find.uniqueSort = jQuery.uniqueSort; + + /* eslint-enable */ + +} )(); + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + + + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + // Single element + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + // Arraylike of elements (jQuery, arguments, Array) + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over <tag> to avoid XSS via location.hash (trac-9521) + // Strict HTML recognition (trac-11290: must start with <) + // Shortcut simple #id case for speed + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + // Inject the element directly into the jQuery object + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + // Positional selectors never match, since there's no _selection_ context + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + // Don't pass non-elements to jQuery#find + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + // Support: IE 11+ + // <object> elements with no `data` attribute has an object + // `contentDocument` with a `null` prototype. + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = locked || options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + // Check for promise aspect first to privilege synchronous behavior + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + // Other thenables + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + // Other non-thenables + } else { + + // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: + // * false: [ value ].slice( 0 ) => resolve( value ) + // * true: [ value ].slice( 1 ) => resolve() + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + // For Promises/A+, convert exceptions into rejections + // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in + // Deferred#then to conditionally suppress rejection. + } catch ( value ) { + + // Support: Android 4.0 only + // Strict mode functions invoked without .call/.apply get global-object context + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, callbacks, + // ... .then handlers, argument index, [final state] + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + // Keep pipe for back-compat + pipe: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + // Map tuples (progress, done, fail) to arguments (done, fail, progress) + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + // deferred.progress(function() { bind to newDefer or newDefer.notify }) + // deferred.done(function() { bind to newDefer or newDefer.resolve }) + // deferred.fail(function() { bind to newDefer or newDefer.reject }) + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + // Support: Promises/A+ section 2.3.3.3.3 + // https://promisesaplus.com/#point-59 + // Ignore double-resolution attempts + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + // Support: Promises/A+ section 2.3.1 + // https://promisesaplus.com/#point-48 + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + // Support: Promises/A+ sections 2.3.3.1, 3.5 + // https://promisesaplus.com/#point-54 + // https://promisesaplus.com/#point-75 + // Retrieve `then` only once + then = returned && + + // Support: Promises/A+ section 2.3.4 + // https://promisesaplus.com/#point-64 + // Only check objects and functions for thenability + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + // Handle a returned thenable + if ( isFunction( then ) ) { + + // Special processors (notify) just wait for resolution + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + // Normal processors (resolve) also hook into progress + } else { + + // ...and disregard older resolution values + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + // Handle all other returned values + } else { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + // Process the value(s) + // Default process is resolve + ( special || deferred.resolveWith )( that, args ); + } + }, + + // Only normal processors (resolve) catch and reject exceptions + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.error ); + } + + // Support: Promises/A+ section 2.3.3.3.4.1 + // https://promisesaplus.com/#point-61 + // Ignore post-resolution exceptions + if ( depth + 1 >= maxDepth ) { + + // Only substitute handlers pass on context + // and multiple values (non-spec behavior) + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + // Support: Promises/A+ section 2.3.3.3.1 + // https://promisesaplus.com/#point-57 + // Re-resolve promises immediately to dodge false rejection from + // subsequent errors + if ( depth ) { + process(); + } else { + + // Call an optional hook to record the error, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getErrorHook ) { + process.error = jQuery.Deferred.getErrorHook(); + + // The deprecated alias of the above. While the name suggests + // returning the stack, not an error instance, jQuery just passes + // it directly to `console.warn` so both will work; an instance + // just better cooperates with source maps. + } else if ( jQuery.Deferred.getStackHook ) { + process.error = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + // progress_handlers.add( ... ) + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + // fulfilled_handlers.add( ... ) + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + // rejected_handlers.add( ... ) + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + // promise.progress = list.add + // promise.done = list.add + // promise.fail = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( + function() { + + // state = "resolved" (i.e., fulfilled) + // state = "rejected" + state = stateString; + }, + + // rejected_callbacks.disable + // fulfilled_callbacks.disable + tuples[ 3 - i ][ 2 ].disable, + + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + + // progress_callbacks.lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock + ); + } + + // progress_handlers.fire + // fulfilled_handlers.fire + // rejected_handlers.fire + list.add( tuple[ 3 ].fire ); + + // deferred.notify = function() { deferred.notifyWith(...) } + // deferred.resolve = function() { deferred.resolveWith(...) } + // deferred.reject = function() { deferred.rejectWith(...) } + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + // deferred.notifyWith = list.fireWith + // deferred.resolveWith = list.fireWith + // deferred.rejectWith = list.fireWith + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( singleValue ) { + var + + // count of uncompleted subordinates + remaining = arguments.length, + + // count of unprocessed arguments + i = remaining, + + // subordinate fulfillment data + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + // the primary Deferred + primary = jQuery.Deferred(), + + // subordinate callback factory + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + primary.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + // Single- and empty arguments are adopted like Promise.resolve + if ( remaining <= 1 ) { + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, + !remaining ); + + // Use .then() to unwrap secondary thenables (cf. gh-3000) + if ( primary.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return primary.then(); + } + } + + // Multiple arguments are aggregated like Promise.all array elements + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); + } + + return primary.promise(); + } +} ); + + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +// If `jQuery.Deferred.getErrorHook` is defined, `asyncError` is an error +// captured before the async barrier to get the original error cause +// which may otherwise be hidden. +jQuery.Deferred.exceptionHook = function( error, asyncError ) { + + // Support: IE 8 - 9 only + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, + error.stack, asyncError ); + } +}; + + + + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + + + + +// The deferred used on DOM ready +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + // Wrap jQuery.readyException in a function so that the lookup + // happens at the time of error handling instead of callback + // registration. + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See trac-6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +// The ready event handler and self cleanup method +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +// Catch cases where $(document).ready() is called +// after the browser event has already occurred. +// Support: IE <=9 - 10 only +// Older IE sometimes signals "interactive" too soon +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + +} else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); +} + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + // Gets + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (trac-9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see trac-8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + // Always use camelCase key (gh-2257) + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + // Always use camelCase key (gh-2257) + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + // Support array or space separated string of keys + if ( Array.isArray( key ) ) { + + // If key is an array of keys... + // We always set camelCase keys, so remove that. + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <=35 - 45 + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + // Only convert to a number if it doesn't change the string + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE 11 only + // The attrs elements can be null (trac-14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // The key will always be camelCased in Data + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each( function() { + + // We always store the camelCased key + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + // isHiddenWithinTree might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + + // Inline style trumps all + return elem.style.display === "none" || + elem.style.display === "" && + + // Otherwise, check computed style + // Support: Firefox <=43 - 45 + // Disconnected elements can have computed display: none, so first confirm that elem is + // in the document. + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + // Determine new display value for elements that need to change + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + // Since we force visibility upon cascade-hidden elements, an immediate (and slow) + // check is required in this first loop unless we have a nonempty display value (either + // inline or about-to-be-restored) + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + // Remember what we're overwriting + dataPriv.set( elem, "display", display ); + } + } + } + + // Set the display of the elements in a second loop to avoid constant reflow + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0 - 4.3 only + // Check state lost if the name is set (trac-11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (trac-14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Android <=4.1 only + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE <=11 only + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = "<textarea>x</textarea>"; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces <option> tags with their contents when inserted outside of + // the select element. + div.innerHTML = "<option></option>"; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (trac-13200) +var wrapMap = { + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting <tbody> or other required elements. + thead: [ 1, "<table>", "</table>" ], + col: [ 2, "<table><colgroup>", "</colgroup></table>" ], + tr: [ 2, "<table><tbody>", "</tbody></table>" ], + td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: IE <=9 only +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "<select multiple='multiple'>", "</select>" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (trac-15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (trac-12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Only attach events to objects that accept data + if ( !acceptData( elem ) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Ensure that invalid selectors throw exceptions at attach time + // Evaluate against documentElement in case elem is a non-element node (e.g., document) + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + if ( delegateCount && + + // Support: IE <=9 + // Black-hole SVG <use> instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (trac-13208) + // Don't process clicks on disabled elements (trac-6911, trac-8165, trac-11382, trac-11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (trac-13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", true ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, isSetup ) { + + // Missing `isSetup` indicates a trigger call, which must force setup through jQuery.event.add + if ( !isSetup ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + if ( !saved ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + this[ type ](); + result = dataPriv.get( this, type ); + dataPriv.set( this, type, false ); + + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + + return result; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering + // the native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved ) { + + // ...and capture the result + dataPriv.set( this, type, jQuery.event.trigger( + saved[ 0 ], + saved.slice( 1 ), + this + ) ); + + // Abort handling of the native event by all jQuery handlers while allowing + // native handlers on the same element to run. On target, this is achieved + // by stopping immediate propagation just on the jQuery event. However, + // the native event is re-wrapped by a jQuery one on each level of the + // propagation so the only way to stop it for jQuery is to stop it for + // everyone via native `stopPropagation()`. This is not a problem for + // focus/blur which don't bubble, but it does also stop click on checkboxes + // and radios. We accept this limitation. + event.stopPropagation(); + event.isImmediatePropagationStopped = returnTrue; + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (trac-504, trac-13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + which: true +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + + function focusMappedHandler( nativeEvent ) { + if ( document.documentMode ) { + + // Support: IE 11+ + // Attach a single focusin/focusout handler on the document while someone wants + // focus/blur. This is because the former are synchronous in IE while the latter + // are async. In other browsers, all those handlers are invoked synchronously. + + // `handle` from private data would already wrap the event, but we need + // to change the `type` here. + var handle = dataPriv.get( this, "handle" ), + event = jQuery.event.fix( nativeEvent ); + event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; + event.isSimulated = true; + + // First, handle focusin/focusout + handle( nativeEvent ); + + // ...then, handle focus/blur + // + // focus/blur don't bubble while focusin/focusout do; simulate the former by only + // invoking the handler at the lower level. + if ( event.target === event.currentTarget ) { + + // The setup part calls `leverageNative`, which, in turn, calls + // `jQuery.event.add`, so event handle will already have been set + // by this point. + handle( event ); + } + } else { + + // For non-IE browsers, attach a single capturing handler on the document + // while someone wants focusin/focusout. + jQuery.event.simulate( delegateType, nativeEvent.target, + jQuery.event.fix( nativeEvent ) ); + } + } + + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + var attaches; + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, true ); + + if ( document.documentMode ) { + + // Support: IE 9 - 11+ + // We use the same native handler for focusin & focus (and focusout & blur) + // so we need to coordinate setup & teardown parts between those events. + // Use `delegateType` as the key as `type` is already used by `leverageNative`. + attaches = dataPriv.get( this, delegateType ); + if ( !attaches ) { + this.addEventListener( delegateType, focusMappedHandler ); + } + dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 ); + } else { + + // Return false to allow normal processing in the caller + return false; + } + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + teardown: function() { + var attaches; + + if ( document.documentMode ) { + attaches = dataPriv.get( this, delegateType ) - 1; + if ( !attaches ) { + this.removeEventListener( delegateType, focusMappedHandler ); + dataPriv.remove( this, delegateType ); + } else { + dataPriv.set( this, delegateType, attaches ); + } + } else { + + // Return false to indicate standard teardown should be applied + return false; + } + }, + + // Suppress native focus or blur if we're currently inside + // a leveraged native-event stack + _default: function( event ) { + return dataPriv.get( event.target, type ); + }, + + delegateType: delegateType + }; + + // Support: Firefox <=44 + // Firefox doesn't have focus(in | out) events + // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 + // + // Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 + // focus(in | out) events fire after focus & blur events, + // which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order + // Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 + // + // Support: IE 9 - 11+ + // To preserve relative focusin/focus & focusout/blur event order guaranteed on the 3.x branch, + // attach a single handler for both events in IE. + jQuery.event.special[ delegateType ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ); + + // Support: IE 9 - 11+ + // We use the same native handler for focusin & focus (and focusout & blur) + // so we need to coordinate setup & teardown parts between those events. + // Use `delegateType` as the key as `type` is already used by `leverageNative`. + if ( !attaches ) { + if ( document.documentMode ) { + this.addEventListener( delegateType, focusMappedHandler ); + } else { + doc.addEventListener( type, focusMappedHandler, true ); + } + } + dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ) - 1; + + if ( !attaches ) { + if ( document.documentMode ) { + this.removeEventListener( delegateType, focusMappedHandler ); + } else { + doc.removeEventListener( type, focusMappedHandler, true ); + } + dataPriv.remove( dataHolder, delegateType ); + } else { + dataPriv.set( dataHolder, delegateType, attaches ); + } + } + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // Support: IE <=10 - 11, Edge 12 - 13 only + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /<script|<style|<link/i, + + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + + rcleanScript = /^\s*<!\[CDATA\[|\]\]>\s*$/g; + +// Prefer a tbody over its parent table for containing new rows +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (trac-8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Re-enable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + + // Unwrap a CDATA section containing script contents. This shouldn't be + // needed as in XML documents they're already not visible when + // inspecting element contents and in HTML documents they have no + // meaning but we're preserving that logic for backwards compatibility. + // This will be removed completely in 4.0. See gh-4904. + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew jQuery#find here for performance reasons: + // https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var rcustomProp = /^--/; + + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (trac-15098, trac-14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + + + +( function() { + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computeStyleTests() { + + // This is a singleton, we need to execute it only once + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 + // Some styles come back with percentage values, even though they shouldn't + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + // Nullify the div so it wouldn't be stored in the memory and + // it will also be a sign that checks already performed + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + // Finish early in limited (non-browser) environments + if ( !div.style ) { + return; + } + + // Support: IE <=9 - 11 only + // Style of cloned element affects source element cloned (trac-8908) + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + // Support: IE 9 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Behavior in IE 9 is more subtle than in newer versions & it passes + // some versions of this test; make sure not to make it pass there! + // + // Support: Firefox 70+ + // Only Firefox includes border widths + // in computed dimensions. (gh-4529) + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "box-sizing:content-box;border:1px solid"; + + // Support: Chrome 86+ + // Height set through cssText does not get applied. + // Computed height then comes back as 0. + tr.style.height = "1px"; + trChild.style.height = "9px"; + + // Support: Android 8 Chrome 86+ + // In our bodyBackground.html iframe, + // display for all div elements is set to "inline", + // which causes a problem only in Android 8 Chrome 86. + // Ensuring the div is `display: block` + // gets around this issue. + trChild.style.display = "block"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + isCustomProp = rcustomProp.test( name ), + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, trac-12537) + // .css('--customProperty) (gh-3144) + if ( computed ) { + + // Support: IE <=9 - 11+ + // IE only supports `"float"` in `getPropertyValue`; in computed styles + // it's only available as `"cssFloat"`. We no longer modify properties + // sent to `.css()` apart from camelCasing, so we need to check both. + // Normally, this would create difference in behavior: if + // `getPropertyValue` returns an empty string, the value returned + // by `.css()` would be `undefined`. This is usually the case for + // disconnected elements. However, in IE even disconnected elements + // with no styles return `"none"` for `getPropertyValue( "float" )` + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( isCustomProp && ret ) { + + // Support: Firefox 105+, Chrome <=105+ + // Spec requires trimming whitespace for custom properties (gh-4926). + // Firefox only trims leading whitespace. Chrome just collapses + // both leading & trailing whitespace to a single space. + // + // Fall back to `undefined` if empty string returned. + // This collapses a missing definition with property defined + // and set to an empty string but there's no standard API + // allowing us to differentiate them without a performance penalty + // and returning `undefined` aligns with older jQuery. + // + // rtrimCSS treats U+000D CARRIAGE RETURN and U+000C FORM FEED + // as whitespace while CSS does not, but this is not a problem + // because CSS preprocessing replaces them with U+000A LINE FEED + // (which *is* CSS whitespace) + // https://www.w3.org/TR/css-syntax-3/#input-preprocessing + ret = ret.replace( rtrimCSS, "$1" ) || undefined; + } + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0, + marginDelta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + // Count margin delta separately to only add it after scroll gutter adjustment. + // This is needed to make negative margins work with `outerHeight( true )` (gh-3982). + if ( box === "margin" ) { + marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta + marginDelta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // Support: IE 9 - 11 only + // Use offsetWidth/offsetHeight for when box sizing is unreliable. + // In those cases, the computed value can be trusted to be border-box. + if ( ( !support.boxSizingReliable() && isBorderBox || + + // Support: IE 10 - 11+, Edge 15 - 18+ + // IE/Edge misreport `getComputedStyle` of table rows with width/height + // set in CSS while `offset*` properties report correct values. + // Interestingly, in some cases IE 9 doesn't suffer from this issue. + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + // Fall back to offsetWidth/offsetHeight when value is "auto" + // This happens for inline elements with no explicit setting (gh-3571) + val === "auto" || + + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + // Make sure the element is visible & connected + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + // Normalize "" and auto + val = parseFloat( val ) || 0; + + // Adjust for the element's box model + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val + ) + ) + "px"; +} + +jQuery.extend( { + + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + animationIterationCount: true, + aspectRatio: true, + borderImageSlice: true, + columnCount: true, + flexGrow: true, + flexShrink: true, + fontWeight: true, + gridArea: true, + gridColumn: true, + gridColumnEnd: true, + gridColumnStart: true, + gridRow: true, + gridRowEnd: true, + gridRowStart: true, + lineHeight: true, + opacity: true, + order: true, + orphans: true, + scale: true, + widows: true, + zIndex: true, + zoom: true, + + // SVG-related + fillOpacity: true, + floodOpacity: true, + stopOpacity: true, + strokeMiterlimit: true, + strokeOpacity: true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (trac-7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug trac-9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (trac-7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +// Based off of the plugin by Clint Helfers, with permission. +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // Use proper attribute retrieval (trac-12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + if ( cur.indexOf( " " + className + " " ) < 0 ) { + cur += className + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + removeClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + + // This expression is here for better compressibility (see addClass) + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + // Remove *all* instances + while ( cur.indexOf( " " + className + " " ) > -1 ) { + cur = cur.replace( " " + className + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var classNames, className, i, self, + type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + classNames = classesToArray( value ); + + return this.each( function() { + if ( isValidValue ) { + + // Toggle individual class names + self = jQuery( this ); + + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (trac-14686, trac-14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (trac-2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml, parserErrorElem; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) {} + + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); + } + return xml; +}; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (trac-9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (trac-9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name as the event. + // Don't do default actions on window, that's where global variables be (trac-6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ).filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ).map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +// Support: Safari 8 only +// In Safari 8 documents created via document.implementation.createHTMLDocument +// collapse sibling forms: the second one becomes a child of the first one. +// Because of that, this security measure has to be disabled in Safari 8. +// https://bugs.webkit.org/show_bug.cgi?id=137337 +support.createHTMLDocument = ( function() { + var body = document.implementation.createHTMLDocument( "" ).body; + body.innerHTML = "<form></form><form></form>"; + return body.childNodes.length === 2; +} )(); + + +// Argument "data" should be string of html +// context (optional): If specified, the fragment will be created in this context, +// defaults to document +// keepScripts (optional): If true, will include scripts passed in the html string +jQuery.parseHTML = function( data, context, keepScripts ) { + if ( typeof data !== "string" ) { + return []; + } + if ( typeof context === "boolean" ) { + keepScripts = context; + context = false; + } + + var base, parsed, scripts; + + if ( !context ) { + + // Stop scripts or inline event handlers from being executed immediately + // by using document.implementation + if ( support.createHTMLDocument ) { + context = document.implementation.createHTMLDocument( "" ); + + // Set the base href for the created document + // so any parsed elements with URLs + // are based on the document's URL (gh-2965) + base = context.createElement( "base" ); + base.href = document.location.href; + context.head.appendChild( base ); + } else { + context = document; + } + } + + parsed = rsingleTag.exec( data ); + scripts = !keepScripts && []; + + // Single tag + if ( parsed ) { + return [ context.createElement( parsed[ 1 ] ) ]; + } + + parsed = buildFragment( [ data ], context, scripts ); + + if ( scripts && scripts.length ) { + jQuery( scripts ).remove(); + } + + return jQuery.merge( [], parsed.childNodes ); +}; + + +jQuery.offset = { + setOffset: function( elem, options, i ) { + var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, + position = jQuery.css( elem, "position" ), + curElem = jQuery( elem ), + props = {}; + + // Set position first, in-case top/left are set even on static elem + if ( position === "static" ) { + elem.style.position = "relative"; + } + + curOffset = curElem.offset(); + curCSSTop = jQuery.css( elem, "top" ); + curCSSLeft = jQuery.css( elem, "left" ); + calculatePosition = ( position === "absolute" || position === "fixed" ) && + ( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1; + + // Need to be able to calculate position if either + // top or left is auto and position is either absolute or fixed + if ( calculatePosition ) { + curPosition = curElem.position(); + curTop = curPosition.top; + curLeft = curPosition.left; + + } else { + curTop = parseFloat( curCSSTop ) || 0; + curLeft = parseFloat( curCSSLeft ) || 0; + } + + if ( isFunction( options ) ) { + + // Use jQuery.extend here to allow modification of coordinates argument (gh-1848) + options = options.call( elem, i, jQuery.extend( {}, curOffset ) ); + } + + if ( options.top != null ) { + props.top = ( options.top - curOffset.top ) + curTop; + } + if ( options.left != null ) { + props.left = ( options.left - curOffset.left ) + curLeft; + } + + if ( "using" in options ) { + options.using.call( elem, props ); + + } else { + curElem.css( props ); + } + } +}; + +jQuery.fn.extend( { + + // offset() relates an element's border box to the document origin + offset: function( options ) { + + // Preserve chaining for setter + if ( arguments.length ) { + return options === undefined ? + this : + this.each( function( i ) { + jQuery.offset.setOffset( this, options, i ); + } ); + } + + var rect, win, + elem = this[ 0 ]; + + if ( !elem ) { + return; + } + + // Return zeros for disconnected and hidden (display: none) elements (gh-2310) + // Support: IE <=11 only + // Running getBoundingClientRect on a + // disconnected node in IE throws an error + if ( !elem.getClientRects().length ) { + return { top: 0, left: 0 }; + } + + // Get document-relative position by adding viewport scroll to viewport-relative gBCR + rect = elem.getBoundingClientRect(); + win = elem.ownerDocument.defaultView; + return { + top: rect.top + win.pageYOffset, + left: rect.left + win.pageXOffset + }; + }, + + // position() relates an element's margin box to its offset parent's padding box + // This corresponds to the behavior of CSS absolute positioning + position: function() { + if ( !this[ 0 ] ) { + return; + } + + var offsetParent, offset, doc, + elem = this[ 0 ], + parentOffset = { top: 0, left: 0 }; + + // position:fixed elements are offset from the viewport, which itself always has zero offset + if ( jQuery.css( elem, "position" ) === "fixed" ) { + + // Assume position:fixed implies availability of getBoundingClientRect + offset = elem.getBoundingClientRect(); + + } else { + offset = this.offset(); + + // Account for the *real* offset parent, which can be the document or its root element + // when a statically positioned element is identified + doc = elem.ownerDocument; + offsetParent = elem.offsetParent || doc.documentElement; + while ( offsetParent && + ( offsetParent === doc.body || offsetParent === doc.documentElement ) && + jQuery.css( offsetParent, "position" ) === "static" ) { + + offsetParent = offsetParent.parentNode; + } + if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) { + + // Incorporate borders into its offset, since they are outside its content origin + parentOffset = jQuery( offsetParent ).offset(); + parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true ); + parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true ); + } + } + + // Subtract parent offsets and element margins + return { + top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), + left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true ) + }; + }, + + // This method will return documentElement in the following cases: + // 1) For the element inside the iframe without offsetParent, this method will return + // documentElement of the parent window + // 2) For the hidden or detached element + // 3) For body or html element, i.e. in case of the html node - it will return itself + // + // but those exceptions were never presented as a real life use-cases + // and might be considered as more preferable results. + // + // This logic, however, is not guaranteed and can change at any point in the future + offsetParent: function() { + return this.map( function() { + var offsetParent = this.offsetParent; + + while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) { + offsetParent = offsetParent.offsetParent; + } + + return offsetParent || documentElement; + } ); + } +} ); + +// Create scrollLeft and scrollTop methods +jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { + var top = "pageYOffset" === prop; + + jQuery.fn[ method ] = function( val ) { + return access( this, function( elem, method, val ) { + + // Coalesce documents and windows + var win; + if ( isWindow( elem ) ) { + win = elem; + } else if ( elem.nodeType === 9 ) { + win = elem.defaultView; + } + + if ( val === undefined ) { + return win ? win[ prop ] : elem[ method ]; + } + + if ( win ) { + win.scrollTo( + !top ? val : win.pageXOffset, + top ? val : win.pageYOffset + ); + + } else { + elem[ method ] = val; + } + }, method, val, arguments.length ); + }; +} ); + +// Support: Safari <=7 - 9.1, Chrome <=37 - 49 +// Add the top/left cssHooks using jQuery.fn.position +// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 +// Blink bug: https://bugs.chromium.org/p/chromium/issues/detail?id=589347 +// getComputedStyle returns percent when specified for top/left/bottom/right; +// rather than make the css module depend on the offset module, just check for it here +jQuery.each( [ "top", "left" ], function( _i, prop ) { + jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, + function( elem, computed ) { + if ( computed ) { + computed = curCSS( elem, prop ); + + // If curCSS returns percentage, fallback to offset + return rnumnonpx.test( computed ) ? + jQuery( elem ).position()[ prop ] + "px" : + computed; + } + } + ); +} ); + + +// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods +jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { + jQuery.each( { + padding: "inner" + name, + content: type, + "": "outer" + name + }, function( defaultExtra, funcName ) { + + // Margin is only for outerHeight, outerWidth + jQuery.fn[ funcName ] = function( margin, value ) { + var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), + extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); + + return access( this, function( elem, type, value ) { + var doc; + + if ( isWindow( elem ) ) { + + // $( window ).outerWidth/Height return w/h including scrollbars (gh-1729) + return funcName.indexOf( "outer" ) === 0 ? + elem[ "inner" + name ] : + elem.document.documentElement[ "client" + name ]; + } + + // Get document width or height + if ( elem.nodeType === 9 ) { + doc = elem.documentElement; + + // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], + // whichever is greatest + return Math.max( + elem.body[ "scroll" + name ], doc[ "scroll" + name ], + elem.body[ "offset" + name ], doc[ "offset" + name ], + doc[ "client" + name ] + ); + } + + return value === undefined ? + + // Get width or height on the element, requesting but not forcing parseFloat + jQuery.css( elem, type, extra ) : + + // Set width or height on the element + jQuery.style( elem, type, value, extra ); + }, type, chainable ? margin : undefined, chainable ); + }; + } ); +} ); + + +jQuery.fn.extend( { + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length === 1 ? + this.off( selector, "**" ) : + this.off( types, selector || "**", fn ); + }, + + hover: function( fnOver, fnOut ) { + return this + .on( "mouseenter", fnOver ) + .on( "mouseleave", fnOut || fnOver ); + } +} ); + +jQuery.each( + ( "blur focus focusin focusout resize scroll click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup contextmenu" ).split( " " ), + function( _i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + } +); + + + + +// Support: Android <=4.0 only +// Make sure we trim BOM and NBSP +// Require that the "whitespace run" starts from a non-whitespace +// to avoid O(N^2) behavior when the engine would try matching "\s+$" at each space position. +var rtrim = /^[\s\uFEFF\xA0]+|([^\s\uFEFF\xA0])[\s\uFEFF\xA0]+$/g; + +// Bind a function to a context, optionally partially applying any +// arguments. +// jQuery.proxy is deprecated to promote standards (specifically Function#bind) +// However, it is not slated for removal any time soon +jQuery.proxy = function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; +}; + +jQuery.holdReady = function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } +}; +jQuery.isArray = Array.isArray; +jQuery.parseJSON = JSON.parse; +jQuery.nodeName = nodeName; +jQuery.isFunction = isFunction; +jQuery.isWindow = isWindow; +jQuery.camelCase = camelCase; +jQuery.type = toType; + +jQuery.now = Date.now; + +jQuery.isNumeric = function( obj ) { + + // As of jQuery 3.0, isNumeric is limited to + // strings and numbers (primitives or objects) + // that can be coerced to finite numbers (gh-2662) + var type = jQuery.type( obj ); + return ( type === "number" || type === "string" ) && + + // parseFloat NaNs numeric-cast false positives ("") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + !isNaN( obj - parseFloat( obj ) ); +}; + +jQuery.trim = function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "$1" ); +}; + + + +// Register as a named AMD module, since jQuery can be concatenated with other +// files that may use define, but not via a proper concatenation script that +// understands anonymous AMD modules. A named AMD is safest and most robust +// way to register. Lowercase jquery is used because AMD module names are +// derived from file names, and jQuery is normally delivered in a lowercase +// file name. Do this after creating the global so that if an AMD module wants +// to call noConflict to hide this version of jQuery, it will work. + +// Note that for maximum portability, libraries that are not jQuery should +// declare themselves as anonymous modules, and avoid setting a global if an +// AMD loader is present. jQuery is a special case. For more information, see +// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon + +if ( typeof define === "function" && define.amd ) { + define( "jquery", [], function() { + return jQuery; + } ); +} + + + + +var + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$; + +jQuery.noConflict = function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; +}; + +// Expose jQuery and $ identifiers, even in AMD +// (trac-7102#comment:10, https://github.com/jquery/jquery/pull/557) +// and CommonJS for browser emulators (trac-13566) +if ( typeof noGlobal === "undefined" ) { + window.jQuery = window.$ = jQuery; +} + + + + +return jQuery; +} ); diff --git a/src/util.rs b/src/util.rs @@ -7,6 +7,7 @@ use rocket::{ response::{self, Responder}, Request, Response, }; +use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor}; use std::{error, io::Cursor, ops::Deref, string::ToString}; use tokio::{ runtime::Handle, @@ -208,7 +209,9 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache res.set_raw_header("Cache-Control", cache_control_header); let time_now = chrono::Local::now(); let expiry_time = time_now - .checked_add_signed(chrono::Duration::seconds(self.ttl.try_into().unwrap())) + .checked_add_signed( + chrono::TimeDelta::try_seconds(self.ttl.try_into().unwrap()).unwrap(), + ) .expect("Duration add overflowed"); res.set_raw_header("Expires", format_datetime_http(&expiry_time)); Ok(res) @@ -256,16 +259,6 @@ pub fn get_uuid() -> String { uuid::Uuid::new_v4().to_string() } use std::str::FromStr; -#[inline] -fn upcase_first(s: &str) -> String { - let mut c = s.chars(); - c.next().map_or_else(String::new, |f| { - let mut val = f.to_uppercase().collect::<String>(); - val.push_str(c.as_str()); - val - }) -} -#[inline] fn lcase_first(s: &str) -> String { let mut c = s.chars(); c.next().map_or_else(String::new, |f| { @@ -304,32 +297,37 @@ fn format_datetime_http(dt: &DateTime<Local>) -> String { // offset (which would always be 0 in UTC anyway) expiry_time.to_rfc2822().replace("+0000", "GMT") } -use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor}; use serde_json::{self, Value}; type JsonMap = serde_json::Map<String, Value>; -#[derive(Deserialize)] -pub struct UpCase<T: DeserializeOwned> { - #[serde(deserialize_with = "upcase_deserialize")] +#[derive(Serialize, Deserialize)] +pub struct LowerCase<T: DeserializeOwned> { + #[serde(deserialize_with = "lowercase_deserialize")] #[serde(flatten)] pub data: T, } -// https://github.com/serde-rs/serde/issues/586 -fn upcase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error> +impl Default for LowerCase<Value> { + fn default() -> Self { + Self { data: Value::Null } + } +} + +pub fn lowercase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error> where T: DeserializeOwned, D: Deserializer<'de>, { - let d = deserializer.deserialize_any(UpCaseVisitor)?; + let d = deserializer.deserialize_any(LowerCaseVisitor)?; T::deserialize(d).map_err(de::Error::custom) } -struct UpCaseVisitor; +struct LowerCaseVisitor; -impl<'de> Visitor<'de> for UpCaseVisitor { +impl<'de> Visitor<'de> for LowerCaseVisitor { type Value = Value; - fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("an object or an array") } fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> @@ -339,7 +337,7 @@ impl<'de> Visitor<'de> for UpCaseVisitor { let mut result_map = JsonMap::new(); while let Some((key, value)) = map.next_entry()? { - result_map.insert(upcase_first(key), upcase_value(value)); + result_map.insert(_process_key(key), convert_json_key_lcase_first(value)); } Ok(Value::Object(result_map)) @@ -351,39 +349,47 @@ impl<'de> Visitor<'de> for UpCaseVisitor { let mut result_seq = Vec::<Value>::new(); while let Some(value) = seq.next_element()? { - result_seq.push(upcase_value(value)); + result_seq.push(convert_json_key_lcase_first(value)); } Ok(Value::Array(result_seq)) } } -fn upcase_value(value: Value) -> Value { - if let Value::Object(map) = value { - let mut new_value = Value::Object(serde_json::Map::new()); - for (key, val) in map { - let processed_key = _process_key(&key); - new_value[processed_key] = upcase_value(val); - } - new_value - } else if let Value::Array(array) = value { - // Initialize array with null values - let mut new_value = Value::Array(vec![Value::Null; array.len()]); - for (index, val) in array.into_iter().enumerate() { - new_value[index] = upcase_value(val); - } - new_value - } else { - value - } -} - // Inner function to handle a special case for the 'ssn' key. // This key is part of the Identity Cipher (Social Security Number) fn _process_key(key: &str) -> String { match key.to_lowercase().as_ref() { - "ssn" => "SSN".into(), - _ => self::upcase_first(key), + "ssn" => "ssn".into(), + _ => self::lcase_first(key), + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum NumberOrString { + Number(i64), + String(String), +} + +impl NumberOrString { + pub fn into_string(self) -> String { + match self { + Self::Number(n) => n.to_string(), + Self::String(s) => s, + } + } + pub fn into_i32(self) -> Result<i32, crate::Error> { + use std::num::ParseIntError as PIE; + match self { + Self::Number(n) => match i32::try_from(n) { + Ok(n) => Ok(n), + Err(_) => err!("Number does not fit in i32"), + }, + Self::String(s) => s + .parse() + .map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())), + } } } @@ -430,32 +436,38 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { match src_json { Value::Array(elm) => { let mut new_array: Vec<Value> = Vec::with_capacity(elm.len()); + for obj in elm { new_array.push(convert_json_key_lcase_first(obj)); } Value::Array(new_array) } + Value::Object(obj) => { let mut json_map = JsonMap::new(); - for tup in obj { - match tup { + for (key, value) in obj { + match (key, value) { (key, Value::Object(elm)) => { - let inner_value = convert_json_key_lcase_first(Value::Object(elm.clone())); - json_map.insert(lcase_first(key.as_str()), inner_value); + let inner_value = convert_json_key_lcase_first(Value::Object(elm)); + json_map.insert(_process_key(&key), inner_value); } + (key, Value::Array(elm)) => { let mut inner_array: Vec<Value> = Vec::with_capacity(elm.len()); for inner_obj in elm { - inner_array.push(convert_json_key_lcase_first(inner_obj.clone())); + inner_array.push(convert_json_key_lcase_first(inner_obj)); } - json_map.insert(lcase_first(key.as_str()), Value::Array(inner_array)); + + json_map.insert(_process_key(&key), Value::Array(inner_array)); } + (key, value) => { - json_map.insert(lcase_first(key.as_str()), value.clone()); + json_map.insert(_process_key(&key), value); } } } + Value::Object(json_map) } value @ (Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)) => value,