diff --git a/README.md b/README.md index ebba20da41..676398b72e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ [![MyGet (nightly builds)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) [![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) -ABP Framework is a complete **infrastructure** based on the **ASP.NET Core** to create **modern web applications** and **APIs** by following the software development **best practices** and the **latest technologies**. +[![ABP Discord server](https://img.shields.io/discord/951497912645476422?label=Discord)](https://discord.gg/abp) + +ABP Framework is a complete **infrastructure** based on the **ASP.NET Core** to create **modern web applications** and **APIs** by following the software development **best practices** and the **latest technologies**. Check out https://abp.io ## Getting Started diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 1dc0d7c1f7..9569a08657 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -393,6 +393,7 @@ "StartTime": "Start Time", "EndTime": "End Time", "CreateABookDiscount": "Create a book discount", - "BookDiscountDeletionConfirmationMessage": "Are you sure you want to delete this book discount?" + "BookDiscountDeletionConfirmationMessage": "Are you sure you want to delete this book discount?", + "CustomPaymentFlexSwitchDescription": "With license" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index e898619b64..205bc10d05 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -26,6 +26,8 @@ "Volo.AbpIo.Domain:030009": "User not found!", "Volo.AbpIo.Domain:030010": "To purchase the trial license, you first need to activate your trial license!", "Volo.AbpIo.Domain:030011": "You cannot delete a trial license when it is purchased!", + "Volo.AbpIo.Domain:070000": "The organization name can only contain latin letters, numbers, dots and hyphens!", + "Volo.AbpIo.Domain:070001": "The company name can only contain latin letters, numbers, dots, space and hyphens!", "WantToLearn?": "Want to learn?", "ReadyToGetStarted?": "Ready to get started?", "JoinOurCommunity": "Join our community", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index a977671501..43503f989c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -199,6 +199,7 @@ "WhatHappensWhenLicenseEndsExplanation5": "You can not use the ABP Suite.", "WhatHappensWhenLicenseEndsExplanation6": "You can not get the premium support anymore.", "WhatHappensWhenLicenseEndsExplanation7": "You can extend (renew) your license if you want to continue getting these benefits. If you extend your license within 1 month after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount.", + "WhatHappensWhenLicenseEndsExplanation8": "The ABP projects you generated are not stored on our servers. Therefore, it is your responsibility to keep the source code you download. When your license expires, there's no way to get your generated ABP project source code.", "WhenShouldIRenewMyLicense": "When should I renew my license?", "WhenShouldIRenewMyLicenseExplanation": "If you renew your license within 1 month after your license expires, the following discounts will be applied: Team License {0}% discount, Business License {1}% discount, Enterprise License {2}% discount. However, if you renew your license after 1 month since the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.", "TrialPlan": "Do you have a trial plan?", @@ -378,8 +379,6 @@ "TaxNoValidationMessage": "TAX/VAT No is too long!", "NotesValidationMessage": "Notes field is too long!", "CheckYourBillingInfo": "You can create your invoice only once! Check your billing information before creating your invoice.", - "Volo.AbpIo.Commercial:030000": "You already used your trial period.", - "Volo.AbpIo.Commercial:030001": "This organization name already exists.", "StartYourFreeTrial": "Start your free trial", "TrialLicenseModelInvalidErrorMessage": "One of the following fields is invalid: Country Name, Company Size, Industry or Purpose Of Usage.", "Trial": "Trial", @@ -487,10 +486,8 @@ "BackOfficeApplicationExplanation": "The actual web application of your system, with multiple UI framework options. You can create any kind of business application.", "LandingWebsite": "Landing Website", "LandingWebsiteExplanation": "A generic landing/public website that can be used for several purposes, like introducing your company, selling your products, etc.", - "ABPFrameworkEBook": "E-Book: Mastering ABP Framework", + "ABPFrameworkEBook": "Mastering ABP Framework e-book", "MasteringAbpFrameworkEBookDescription": "Included within your ABP Commercial license", - "Volo.AbpIo.Domain:070000": "The organization name can only contain latin letters, numbers, dots and hyphens!", - "Volo.AbpIo.Domain:070001": "The company name can only contain latin letters, numbers, dots, space and hyphens!", "FullName": "Full Name", "LicenseTypeNotCorrect": "The license type is not correct!", "Trainings": "Trainings", @@ -501,11 +498,38 @@ "ForMoreInformationClickHere": "For more information, click here.", "IsGetOnboardingTraining": "Would you like to get onboarding & web application development training?", "OnboardingWebApplicationDevelopmentTrainingMessage": "To schedule your training calendar, please contact {0} after creating the organization", + "CustomPurchaseMessage": "For the next step, click {0} to contact us.", "Note": "Note", "AdditionalNote": "Additional Note", "OnboardingTrainingFaqTitle": "Do you have ABP onboarding training?", "OnboardingTrainingFaqExplanation": "Yes, we have ABP Training Services to help you get your ABP project started fast. You will learn about ABP from an ABP core team member and you will get the skills to begin your ABP project. In the onboarding training, we will explain how to set up your development environment, install the required tools, create a fully functional CRUD page. The training will be live and the Zoom application will be used, and we are open to using other online meeting platforms. The language of the training will be English. You can also ask your questions about ABP during the sessions. A convenient time and date will be planned for both parties. To get more information, contact us at info@abp.io.", "AddBasket": "Add to Basket", - "SendTrainingRequest": "Send Training Request" + "SendTrainingRequest": "Send Training Request", + "OnlyEnglishVersionOfThisDocumentIsTheRecentAndValid": "* The English version of this document is the most up-to-date and the English version will prevail in any dispute.", + "Pricing_Page_Title": "Plans & Pricing", + "Pricing_Page_Description": "Choose the features and functionality your business needs today. Buy an ABP Commercial license and create unlimited projects.", + "Pricing_Page_HurryUp": "Hurry Up!", + "Pricing_Page_BuyLicense": "Buy a license at 2021 prices until January 16!", + "Pricing_Page_ValidForExistingCustomers": "Also valid for existing customers and license renewals.", + "Pricing_Page_Hint1": "The license price includes a certain number of developer seats. If you have more developers, you can always purchase additional seats.", + "Pricing_Page_Hint2": "You can purchase more developer licenses now or in the future. Licenses are seat based, so you can transfer a seat from a developer to another.", + "Pricing_Page_Hint3": "You can develop unlimited count of different products with your license.", + "Pricing_Page_Hint4": "ABP Suite is a tool to assist your development to improve your productivity. It supports generating CRUD pages and creating new projects.", + "Pricing_Page_Hint5": "You can use all the pre-built modules in your applications.", + "Pricing_Page_Hint6": "You can use all the pre-built themes in your applications.", + "Pricing_Page_Hint7": "A startup template is a Visual Studio solution to make you jump-start to your project. All fundamental modules are added and pre-configured for you.", + "Pricing_Page_Hint8": "Mastering ABP Framework e-book explains how to implement .NET solutions with best practices. It is sold on Amazon.com and you can download the book for free within your license.", + "Pricing_Page_Hint9": "You can download the source-code of any module. You may want to add the source code to your solution to make radical changes or just keep it for yourself for security reasons.", + "Pricing_Page_Hint10": "Licenses are for lifetime. That means you can continue to develop your application forever. Accessing to the latest version and getting support are granted within the license period (1 year unless you renew it).", + "Pricing_Page_Hint11": "No restrictions on deployment! You can deploy to as many servers as you want, including the cloud services or on-premises.", + "Pricing_Page_Hint12": "You can update the modules, themes and tools to the latest version within your active license period. After your license expires, you need to renew it, to continue to get updates for bug fixes, new features and enhancements.", + "Pricing_Page_Hint13": "You can get the premium support for one year (you can renew your license to extend it).", + "Pricing_Page_Hint14": "Team and Business licenses have incident/question count limit. If you buy additional developer licenses, your incident limit increases by {0} (for the Team License) or {1} (for the Business License) per developer.", + "Pricing_Page_Hint15": "Only Enterprise License includes private support. You can send e-mail directly to the ABP Team or ask questions on support.abp.io with private ticket option. The private tickets are not visible to the public.", + "Pricing_Page_Hint16": "You can download the source-code of all ABP themes. You may want to add the source code to your solution to make radical changes or just keep it for yourself for security reasons.", + "Pricing_Page_Testimonial_1": "ABP Commercial allowed SC Ventures to deliver a bank-grade multi-tenant silo-database SaaS platform in 9 months to support the accounts receivable / accounts payable supply chain financing of significant value invoices from multiple integrated anchors. The modularity of ABP made it possible for the team to deliver in record time, pass all VAPT, and deploy the containerized micro-services stack via full CI/CD and pipelines into production.", + "Pricing_Page_Testimonial_2": "We are seeing the value of using ABP Commercial to reduce the overhead of custom development projects. And the team is able to unify the code pattern in different project streams. We see more potential in the framework for us to build new features faster than before. We trust we will be constantly seeing the value of leveraging ABP Commercial.", + "Pricing_Page_Testimonial_3": "We love ABP. We don't have to write everything from scratch. We start from out-of-the-box features and just focus on what we really need to write. Also, ABP is well-architected and the code is high quality with fewer bugs. If we would have to write everything we needed on our own, we might have to spend years. Once more things we like is that the new version, or issue fixing, or improvement come out very soon every other week. We don't wait too long.", + "Pricing_Page_Testimonial_4": "ABP Commercial is a fantastic product would recommend. Commercial products to market for our customers in a single configurable platform. The jump start that the framework and tooling provide any team is worth every cent. ABP Commercial was the best fit for our needs." } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json index e4112a4900..0283b963bc 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json @@ -102,6 +102,7 @@ "PostRequestMessageBody": "Here is the list of the requested posts by the Community. Do you want to write a requested post? Please click on the request and join the discussion.", "Language": "Language", "CreatePostLanguageInfo": "Language of the post", - "SeeMore": "See More" + "SeeMore": "See More", + "MemberNotPublishedPostYet": "This member hasn't published any posts yet." } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json index 54cf7e10fe..de04b36283 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -165,6 +165,7 @@ "SortBy": "Sort by", "NoPublishedEventsYet": "No published events yet.", "SubscribeYoutubeChannel": "Subscribe to the Youtube Channel", - "Enum:EventType:0": "Talks" + "Enum:EventType:0": "Talks", + "MemberNotPublishedPostYet": "This member hasn't published any posts yet." } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json index 72d6c5e28d..0b118ed73d 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json @@ -142,6 +142,7 @@ "MinimumSearchContent": "En az 3 karakter girmelisiniz!", "Volo.AbpIo.Domain:060001": "Kaynak URL (\"{PostUrl}\") Github URL'si değil", "Volo.AbpIo.Domain:060002": "Makale İçeriği Github(\"{PostUrl}\") kaynağında mevcut değil.", - "Volo.AbpIo.Domain:060003": "Makale içeriği bulunamadı!" + "Volo.AbpIo.Domain:060003": "Makale içeriği bulunamadı!", + "MemberNotPublishedPostYet": "Bu üye henüz bir gönderi yayınlamadı." } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index 35b755e8c3..d17259241c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -174,6 +174,7 @@ "CreateProjectWizard": "This wizard creates a new project from the startup template which is properly configured to jump start your project.", "TieredOption": "Creates a tiered solution where Web and Http API layers are physically separated. If not checked, creates a layered solution which is less complex and suitable for most scenarios.", "SeparateIdentityServerOption": "Separates the server side into two applications: The first one is for the identity server and the second one is for your server side HTTP API.", + "ProgressiveWebApplicationOption": "Specifies the project as Progressive Web Application", "UseslatestPreVersion": "Uses the latest pre-release version", "ReadTheDocumentation": "Read The Documentation", "Documentation": "Documentation", @@ -296,6 +297,7 @@ "EnterYouEmailToGetNews": "Enter your email to get the latest news about the ABP Framework", "Tiered": "Tiered", "SeparateIdentityServer": "Separate Identity Server", + "ProgressiveWebApplication": "Progressive Web Application", "Preview": "Preview", "CreateANewSolution": "Create a new solution", "ABPFrameworkFeatures": "ABP Framework Features", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json index ad8393fab3..01fe9f8b7b 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json @@ -174,6 +174,8 @@ "CreateProjectWizard": "Bu sihirbaz, projenize atlamak için uygun şekilde yapılandırılmış başlangıç şablonundan yeni bir proje oluşturur.", "TieredOption": "Web ve Http API katmanlarının fiziksel olarak ayrıldığı katmanlı bir çözüm oluşturur. İşaretlenmezse, daha az karmaşık olan ve çoğu senaryo için uygun olan katmanlı bir çözüm oluşturur.", "SeparateIdentityServerOption": "Sunucu tarafını iki uygulamaya ayırır: Birincisi kimlik sunucusu için, ikincisi ise sunucu tarafı HTTP API'niz içindir.", + "ProgressiveWebApplicationOption": "Projeyi Progresif Web Uygulaması olarak ayarlayın.", + "ProgressiveWebApplication": "Progresif Web Uygulaması", "UseslatestPreVersion": "En son yayın öncesi sürümünü kullanır", "ReadTheDocumentation": "Belgeleri okuyun", "Documentation": "belgeler", diff --git a/build/common.ps1 b/build/common.ps1 index d9ebc2f87c..01a7565fba 100644 --- a/build/common.ps1 +++ b/build/common.ps1 @@ -14,6 +14,7 @@ $solutionPaths = @( "../modules/feature-management", "../modules/identity", "../modules/identityserver", + "../modules/openiddict", "../modules/tenant-management", "../modules/audit-logging", "../modules/background-jobs", diff --git a/common.props b/common.props index 9b4b018b94..856291bb50 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 5.3.0-rc.2 + 5.4.0 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/POST.md b/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/POST.md new file mode 100644 index 0000000000..e2ad154bdd --- /dev/null +++ b/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/POST.md @@ -0,0 +1,243 @@ +# Consuming HTTP APIs from a .NET Client Using ABP's Client Proxy System + +In this article, I will explain how to consume HTTP APIs from a .NET application using ABP's [dynamic](https://docs.abp.io/en/abp/latest/API/Dynamic-CSharp-API-Clients) and [static](https://docs.abp.io/en/abp/latest/API/Static-CSharp-API-Clients) client-side proxy systems. I will start by creating a new project and consume the HTTP APIs from a .NET console application using dynamic client proxies. Then I will switch to static client proxies. Finally, I will glance at the differences and similarities between static and dynamic generic proxies. + +Here the main benefits of using the client-side proxy system (either dynamic or static): + +* Automatically maps C# method calls to remote server HTTP calls by considering the HTTP method, route, query string parameters, request payload and other details. +* Authenticates the HTTP Client by adding an access token to the HTTP header. +* Serializes to and deserialize from JSON. +* Handles HTTP API versioning. +* Adds correlation id, current tenant id and the current culture to the request. +* Properly handles the error messages sent by the server and throws proper exceptions. + +## Create a new ABP application with the ABP CLI +Firstly create a new solution via [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): + +```shell +abp new Acme.BookStore +``` + +> See ABP's [Getting Started document](https://docs.abp.io/en/abp/latest/Getting-Started-Setup-Environment?UI=MVC&DB=EF&Tiered=No) to learn how to create and run your application, if you haven't done it before. + +## Create the application service interface +I will start by creating an application service and exposing it as an HTTP API to be consumed by remote clients. First, define an interface for the application service; Create an `IBookAppService` interface in the `Books` folder (namespace) of the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Acme.BookStore.Books +{ + public interface IBookAppService : IApplicationService + { + Task> GetListAsync(); + } +} +```` + +Also add a `BookDto` class inside the same `Books` folder: + +```csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore.Books +{ + public class BookDto + { + public string Name { get; set; } + public string AuthorName { get; set; } + public float Price { get; set; } + } +} +``` + +## Implement the application service +It is time to implement the `IBookAppService` interface. Create a new class named `BookAppService` in the `Books` namespace (folder) of the `Acme.BookStore.Application` project: + +```csharp +using Acme.BookStore.Permissions; +using Microsoft.AspNetCore.Authorization; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Acme.BookStore.Books +{ + public class BookAppService : ApplicationService, IBookAppService + { + public Task> GetListAsync() + { + var bookDtos = new List() + { + new BookDto(){ Name = "Hunger", AuthorName ="Knut Hamsun", Price = 50}, + new BookDto(){ Name = "Crime and Punishment", AuthorName ="Dostoevsky", Price = 60}, + new BookDto(){ Name = "For Whom the Bell Tolls", AuthorName ="Ernest Hemingway", Price = 70} + }; + return Task.FromResult(new PagedResultDto( + bookDtos.Count, + bookDtos + )); + } + } +} +``` +It simply returns a list of books. You probably want to get the books from a database, but it doesn't matter for this article. If you want it, you can fully implement [this tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF). + +## Consume the app service from the console application +The startup solution comes with an example .NET console application (`Acme.BookStore.HttpApi.Client.ConsoleTestApp`) that is fully configured to consume your HTTP APIs remotely. Change `ClientDemoService` as shown in the following `Acme.BookStore.HttpApi.Client.ConsoleTestApp` project (it is under the `test` folder). + +```csharp +using Acme.BookStore.Books; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.DependencyInjection; + +namespace Acme.BookStore.HttpApi.Client.ConsoleTestApp; + +public class ClientDemoService : ITransientDependency +{ + private readonly IBookAppService _bookAppService; + + public ClientDemoService(IBookAppService bookAppService ) + { + _bookAppService = bookAppService; + } + + public async Task RunAsync() + { + var listOfBooks = await _bookAppService.GetListAsync(new PagedAndSortedResultRequestDto()); + Console.WriteLine($"Books: {string.Join(", ", listOfBooks.Items.Select(p => p.Name).ToList())}"); + } +} +``` + +We are basically injecting the `IBookAppService` interface to consume the remote service. ABP handles all the details (performing HTTP request, deserializing the resulting JSON object, etc) for us. + +You can run the application to see the output: + +``` +Books: Hunger, Crime and Punishment, For Whom the Bell Tolls +``` + +## Convert the application to use static client proxies +The [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application) comes pre-configured for the **dynamic** client proxy generation, in the `HttpApi.Client` project. If you want to switch to the **static** client proxies, you should change `context.Services.AddHttpClientProxies` to `context.Services.AddStaticHttpClientProxies` in the module class of your `HttpApi.Client` project: + +```csharp +public class BookStoreHttpApiClientModule : AbpModule +{ + public const string RemoteServiceName = "Default"; + + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Other configurations... + + context.Services.AddStaticHttpClientProxies( + typeof(BookStoreApplicationContractsModule).Assembly, + RemoteServiceName + ); + } +} +``` + +The `AddStaticHttpClientProxies` method gets an assembly, finds all service interfaces in the given assembly, and prepares for static client proxy generation. + + +Now you're ready to generate the client proxy code by running the following command in the root folder of your client project **while your server-side project is running**: + +````bash +abp generate-proxy -t csharp -u http://localhost:44397/ +```` + +> The URL (`-u` parameter's value) might be different for your application. It should be the server's root URL. + +You should see the generated files under the selected folder: + +![files of the static proxy](./static-proxy.png) + +Now you can run the console client application again. You should see the same output: + +```` +Books: Hunger, Crime and Punishment, For Whom the Bell Tolls +```` + +## Add authorization +The ABP Framework provides an [authorization system](https://docs.abp.io/en/abp/latest/Authorization) based on [ASP.NET Core's authorization infrastructure](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/introduction). We can define permissions and restrict access to some of our application's functionalities, so only the allowed users/clients can use these functionalities. Here, I will define a permission to be able to get the list of books. + +### Defining a permission + +Under `Acme.BookStore.Application.Contracts` open `BookStorePermissions` and paste the below code: +```csharp +namespace Acme.BookStore.Permissions; + +public static class BookStorePermissions +{ + public const string GroupName = "BookStore"; + + public static class Books + { + public const string Default = GroupName + ".Books"; + } + +} +``` +You also need to change `BookStorePermissionDefinitionProvider` under the same folder and project as follows: +```csharp +using Acme.BookStore.Localization; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName); + bookStoreGroup.AddPermission(BookStorePermissions.Books.Default); + } +} +``` +### Authorizing the application service + +We can now add the `[Authorize(BookStorePermissions.Books.Default)]` attribute to the `BookAppService` class: + +```csharp +[Authorize(BookStorePermissions.Books.Default)] +public class BookAppService : ApplicationService, IBookAppService +{ + ... +} +``` + +If you run the server now, then run the console client application, you will see the following error on the console application: + +``` +Unhandled exception. Volo.Abp.Http.Client.AbpRemoteCallException: Forbidden at +Volo.Abp.Http.Client.ClientProxying.ClientProxyBase`1 +.ThrowExceptionForResponseAsync(HttpResponseMessage response)... +``` + +To fix the problem, we should grant permission to the admin user. We are granting permission to the admin user because the console application is configured to use the Resource Owner Password Grant Flow. That means the client application is consuming services on behalf of the admin user. You can see the configuration in the `appsettings.json` file of the console application. + +### Granting the permission + +Once you define the permissions, you can see them on the permission management modal. + +Go to the Administration -> Identity -> Roles page, select the Permissions action for the admin role to open the permission management modal: +![persmisson](./permission.png) +Grant the permissions you want and save the modal. + +## Dynamic vs static proxies + +Static generic proxies provide **better performance** because they don't need to run on runtime, but you should **re-generate** them once you change the API endpoint definition. Dynamic generic proxies don't need to be re-generated because they work on the runtime but they have a slight performance penalty. + +## Further Reading +In this tutorial, I explained how you can create an example project and apply a static client proxy instead of a dynamic client proxy. I also summarized the differences between both approaches. If you want to get more information, you can read the following documents: + +* [Static C# API Client Proxies](https://docs.abp.io/en/abp/latest/API/Static-CSharp-API-Clients) +* [Dynamic C# API Client Proxies](https://docs.abp.io/en/abp/latest/API/Dynamic-CSharp-API-Clients) +* [Web Application Development Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF) diff --git a/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/permission.png b/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/permission.png new file mode 100644 index 0000000000..60e6667993 Binary files /dev/null and b/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/permission.png differ diff --git a/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/static-proxy.png b/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/static-proxy.png new file mode 100644 index 0000000000..083c6cedfe Binary files /dev/null and b/docs/en/Community-Articles/2022-05-16-Consuming-Rest-Api-By-Using-Static-Proxy/static-proxy.png differ diff --git a/docs/en/Contribution/How-to-Contribute-abp.io-as-a-frontend-developer.md b/docs/en/Contribution/How-to-Contribute-abp.io-as-a-frontend-developer.md new file mode 100644 index 0000000000..2d9cd64065 --- /dev/null +++ b/docs/en/Contribution/How-to-Contribute-abp.io-as-a-frontend-developer.md @@ -0,0 +1,54 @@ +# How to contribute to abp.io as a frontend developer + +## How to setup development environment + +### Pre-requirements + +- Dotnet core SDK https://dotnet.microsoft.com/en-us/download +- Nodejs LTS https://nodejs.org/en/ +- Docker https://docs.docker.com/engine/install +- Angular CLI. https://angular.io/guide/what-is-angular#angular-cli +- Abp CLI https://docs.abp.io/en/abp/latest/cli +- A code editor + +Note: This arcticle prepare Windows OS. You may change the path type of your OS. an Example + +Windows: `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json` + +Unix: `templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/appsettings.json` + +### Sample docker commands + +You need to install SQL Server and Redis. You can install these programs without docker, but my example uses docker containers. Your computer should have Docker Engine. Then open the terminal en execute the commands one by one. +For the Sql Server + +docker run -v sqlvolume:/var/opt/mssql -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=yourpassword -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-CU3-ubuntu-18.04 + +For the Redis + +docker run -p 6379:6379 -d redis + +Then we are ready to download and execute the code. + +## Folder Structure + +The app has a backend written in .net core (c#) and an angular app. It would help if you ran both of them. + +### Running Backend App + +The path of the Backend app is “templates\app\aspnet-core.” If you want to work with dockerized SQL Server, you should change connection strings for running with docker. The path of the connection string is +`templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator\appsettings.json`. + +Before running the backend, you should run the Db migrator project. The DbMigrator created initial tables and values. The path of DbMigrator is `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.DbMigrator`. Open a terminal in the path and execute the command `dotnet run` in terminal + +One last step before the running the backend is installing client-side libraries. Go to `templates\app\aspnet-core`. Open a terminal in the path and execute the command `abp install-libs` in terminal + +Next step you should go to path of backend host project. The path is `templates\app\aspnet-core\src\MyCompanyName.MyProjectName.HttpApi.HostWithIds`. Open a terminal in the path and execute the command `dotnet run` in terminal + +Your backend should be running successfully + +### Running Frontend App + +There is a demo app. The path of the demo app is `npm\ng-packs\apps\dev-app`. The demo app is connected to the packages with local references. Open the terminal in `npm\ng-packs\apps\dev-app` and execute `yarn` or `npm i` in terminal. After the package installed run `npm start` or `yarn start`. + +The repo uses Nx and packages connected with `local references`. The packages path is `”npm\ng-packs\packages` diff --git a/docs/en/Contribution/Index.md b/docs/en/Contribution/Index.md index bbb34d4eb3..8ebac70c20 100644 --- a/docs/en/Contribution/Index.md +++ b/docs/en/Contribution/Index.md @@ -71,6 +71,10 @@ If you want to make a change on a specific resource file, you can find the file If you find any bug, please [create an issue on the Github repository](https://github.com/abpframework/abp/issues/new). +## Setup Frontend Development Environment + +[How to contribute to abp.io as a frontend developer](How-to-Contribute-abp.io-as-a-frontend-developer.md) + ## See Also -* [ABP Community Talks 2022.4: How can you contribute to the open source ABP Framework?](https://www.youtube.com/watch?v=Wz4Z-O-YoPg&list=PLsNclT2aHJcOsPustEkzG6DywiO8eh0lB) +* [ABP Community Talks 2022.4: How can you contribute to the open source ABP Framework?](https://www.youtube.com/watch?v=Wz4Z-O-YoPg&list=PLsNclT2aHJcOsPustEkzG6DywiO8eh0lB) \ No newline at end of file diff --git a/docs/en/Modules/OpenIddict.md b/docs/en/Modules/OpenIddict.md new file mode 100644 index 0000000000..951a745aab --- /dev/null +++ b/docs/en/Modules/OpenIddict.md @@ -0,0 +1,240 @@ +## ABP OpenIddict Modules + +## How to Install + +TODO: + +## User Interface + +This module implements the domain logic and database integrations, but not provides any UI. Management UI is useful if you need to add applications and scopes on the fly. In this case, you may build the management UI yourself or consider to purchase the [ABP Commercial](https://commercial.abp.io/) which provides the management UI for this module. + +## Relations to Other Modules + +This module is based on the [Identity Module](Identity.md) and have an [integration package](https://www.nuget.org/packages/Volo.Abp.Account.Web.IdentityServer) with the [Account Module](Account.md). + +## The module + +### Demo projects + +In the module's `app` directory there are six projects(including `angular`) + +* `OpenIddict.Demo.Server`: An abp application with integrated modules (has two `clients` and a `scope`). +* `OpenIddict.Demo.API`: ASP NET Core API application using JwtBearer authentication +* `OpenIddict.Demo.Client.Mvc`: ASP NET Core MVC application using `OpenIdConnect` for authentication +* `OpenIddict.Demo.Client.Console`: Use `IdentityModel` to test OpenIddict's various endpoints, and call the api of `OpenIddict.Demo.API` +* `OpenIddict.Demo.Client.BlazorWASM:` ASP NET Core Blazor application using `OidcAuthentication` for authentication +* `angular`: An angular application that integrates the abp ng modules and uses oauth for authentication + +#### How to run? + +Confirm the connection string of `appsettings.json` in the `OpenIddict.Demo.Server` project. Running the project will automatically create the database and initialize the data. +After running the `OpenIddict.Demo.API` project, then you can run the rest of the projects to test. + +### Domain module + +There are four main entities included in this module. + +* OpenIddictApplication: **Represents applications(client)** +* OpenIddictScope: **Represents scopes** +* OpenIddictAuthorization: **Represents authorizations, Track of logical chains of tokens and user consent..** +* OpenIddictToken: **Represents various tokens.** + +Domain also implements four store interfaces in OpenIddict, OpenIddict uses store to manage entities, corresponding to the above four entities, Custom entity repository is used in the store. + + +```cs +//Manager +OpenIddictApplicationManager +OpenIddictScopeManager +OpenIddictAuthorizationManager +OpenIddictTokenManager + +//Store +IOpenIddictApplicationStore +IOpenIddictScopeStore +IOpenIddictAuthorizationStore +IOpenIddictTokenStore + +//Repository +IOpenIddictApplicationRepository +IOpenIddictScopeRepository +IOpenIddictAuthorizationRepository +IOpenIddictTokenRepository +``` + +We enabled most of OpenIddict's features in the `AddOpenIddict` method, You can change OpenIddict's related builder options via `PreConfigure`. + +```cs +PreConfigure(builder => +{ + //builder +}); + +PreConfigure(builder => +{ + //builder +}); + +PreConfigure(builder => +{ + //builder +}); +``` + +#### AbpOpenIddictAspNetCoreOptions + +`UpdateAbpClaimTypes(default: true)`: Updates AbpClaimTypes to be compatible with identity server claims. +`AddDevelopmentEncryptionAndSigningCertificate(default: true)`: Registers (and generates if necessary) a user-specific development encryption/development signing certificate. + +You can also change this options via `PreConfigure`. + +#### Automatically removing orphaned tokens/authorizations + +There is a background task in the `Domain` module (`enabled by default`) that automatically removes orphaned tokens/authorizations, you can configure `TokenCleanupOptions` to manage it. + +### ASP NET Core module + +This module integrates ASP NET Core, with built-in MVC controllers for four protocols. It uses OpenIddict's [Pass-through mode](https://documentation.openiddict.com/guides/index.html#pass-through-mode). + +```cs +AuthorizeController -> connect/authorize +TokenController -> connect/token +LogoutController -> connect/logout +UserInfoController -> connect/userinfo +``` + +> We will implement the related functions of **device flow** in the PRO module.. + +#### How to control claims in access_token and id_token + +You can use the [Claims Principal Factory](https://docs.abp.io/en/abp/latest/Authorization#claims-principal-factory) to add/remove claims to the `ClaimsPrincipal`. + +The `AbpDefaultOpenIddictClaimDestinationsProvider` service will add `Name`, `Email` and `Role` types of Claims to `access_token` and `id_token`, other claims are only added to `access_token` by default, and remove the `SecurityStampClaimType` secret claim of `Identity`. + +You can create a service that inherits from `IAbpOpenIddictClaimDestinationsProvider` and add it to DI to fully control the destinations of claims + +```cs +public class MyClaimDestinationsProvider : IAbpOpenIddictClaimDestinationsProvider, ITransientDependency +{ + public virtual Task SetDestinationsAsync(AbpOpenIddictClaimDestinationsProviderContext context) + { + // ... + return Task.CompletedTask; + } +} + +Configure(options => +{ + options.ClaimDestinationsProvider.Add(); +}); +``` + +For detailed information, please refer to: [OpenIddict claim destinations](https://documentation.openiddict.com/configuration/claim-destinations.html) + +#### About Validation + +The `OpenIddict.Validation.AspNetCore` and `OpenIddict.Validation` are not integrated in the module, we use the authentication component provided by Microsoft. If you are more familiar with it, you can use it in your project. + + +### EF Core module + +Implements the above four repository interfaces. + +### MongoDB module + +Implements the above four repository interfaces. + + +## OpenIddict + +### Documentation + +For more details about OpenIddict, please refer to its official documentation and Github. + +https://documentation.openiddict.com + +https://github.com/openiddict/openiddict-core#resources + +### Token encryption + +https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html + +> By default, OpenIddict enforces encryption for all the token types it supports. While this enforcement cannot be disabled for authorization codes, refresh tokens and device codes for security reasons, it can be relaxed for access tokens when integration with third-party APIs/resource servers is desired. Access token encryption can also be disabled if the resource servers receiving the access tokens don't fully support JSON Web Encryption. + +```cs +PreConfigure(builder => +{ + builder.DisableAccessTokenEncryption(); +}); +``` + +An example of using `SecurityKey` + +> In production, it is recommended to use two RSA certificates, distinct from the certificate(s) used for HTTPS: one for encryption, one for signing. + +```cs +// In OpenIddict Server +PreConfigure(builder => +{ + builder.AddSigningKey(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Abp_OpenIddict_Demo_C40DBB176E78"))); + builder.AddEncryptionKey(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Abp_OpenIddict_Demo_87E33FC57D80"))); +}); + +//In Client AddJwtBearer +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + //Other configuration + + options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Abp_OpenIddict_Demo_C40DBB176E78")); + options.TokenValidationParameters.TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Abp_OpenIddict_Demo_87E33FC57D80")); + }); +``` + + +### PKCE + +https://documentation.openiddict.com/configuration/proof-key-for-code-exchange.html + +### Request/Response process + +I will briefly introduce the principle of OpenIddict so that everyone can quickly understand it. + +The `OpenIddict.Server.AspNetCore` adds an authentication scheme(`Name: OpenIddict.Server.AspNetCore, handler: OpenIddictServerAspNetCoreHandler`) and implements the `IAuthenticationRequestHandler` interface. + +It will be executed first in `AuthenticationMiddleware` and can short-circuit the current request. Otherwise, `DefaultAuthenticateScheme` will be called and continue to execute the pipeline. + +`OpenIddictServerAspNetCoreHandler` will call various built-in handlers(Handling requests and responses), And the handler will process according to the context or skip logic that has nothing to do with it. + +Example a token request: + +``` +POST /connect/token HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + + grant_type=password& + client_id=AbpApp& + client_secret=1q2w3e*& + username=admin& + password=1q2w3E*& + scope=AbpAPI offline_access +``` + +This request will be processed by various handlers. They will confirm the endpoint type of the request, check `http/https`, verify that the request parameters (`client. scope etc`) are valid and exist in the database, etc. Various protocol checks. And build a `OpenIddictRequest` object, If there are any errors, the response content may be set and directly short-circuit the current request. + +If everything is ok, the request will go to our processing controller(eg `TokenController`), we can get an `OpenIddictRequest` from the http request at this time. The rest of our work will be based on this object. + +We may check the `username` and `password` in the request. If it is correct we create a `ClaimsPrincipal` object and return a `SignInResult`, which uses the `OpenIddict.Validation.AspNetCore` authentication scheme name, will calls `OpenIddictServerAspNetCoreHandler` for processing. + +`OpenIddictServerAspNetCoreHandler` do some checks to generate json and replace the http response content. + +The `ForbidResult` `ChallengeResult` are all the above types of processing. + +If you need to customize OpenIddict, you need to replace/delete/add new handlers and make it execute in the correct order. + +Please refer to: +https://documentation.openiddict.com/guides/index.html#events-model + +## Sponsor + +Please consider sponsoring this project: https://github.com/sponsors/kevinchalet diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index dc82876efb..a1a5ac5cce 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -164,6 +164,10 @@ If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method > See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete. +### Ensure Entities Exists + +The `EnsureExistsAsync` extension method accepts entity id or entities query expression to ensure entities exist, otherwise, it will throw `EntityNotFoundException`. + ## Other Generic Repository Types Standard `IRepository` interface exposes the standard `IQueryable` and you can freely query using the standard LINQ methods. This is fine for most of the applications. However, some ORM providers or database systems may not support standard `IQueryable` interface. If you want to use such providers, you can't rely on the `IQueryable`. diff --git a/docs/en/UI/Angular/Loading-Directive.md b/docs/en/UI/Angular/Loading-Directive.md new file mode 100644 index 0000000000..18ff4baaeb --- /dev/null +++ b/docs/en/UI/Angular/Loading-Directive.md @@ -0,0 +1,44 @@ +# Loading Directive + + +You may want to block a part of the UI and show a spinner for a while; the `LoadingDirective` directive makes this for you. `LoadingDirective` has been exposed by the `@abp/ng.theme.shared` package. + + +## Getting Started + +In order to use the `LoadingDirective` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this: + +```js +// ... +import { ThemeSharedModule } from '@abp/ng.theme.shared'; + +@NgModule({ + //... + imports: [..., ThemeSharedModule], +}) +export class MyFeatureModule {} +``` + + +## Usage + +The `LoadingDirective` is easy to use. The directive's selector is **`abpLoading`**. By adding the `abpLoading` attribute to an HTML element, you can activate the `LoadingDirectiveective` for the HTML element when the value is true. + +See an example usage: + +```html +
+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Laboriosam commodi quae aspernatur, + corporis velit et suscipit id consequuntur amet minima expedita cum reiciendis dolorum + cupiditate? Voluptas eaque voluptatum odio deleniti quo vel illum nemo accusamus nulla ratione + impedit dolorum expedita necessitatibus fugiat ullam beatae, optio eum cupiditate ducimus + architecto. +
+``` + + +The `abpLoading` attribute has been added to the `
` element that contains very a long text inside to activate the `LoadingDirective`. + +See the result: + +![Loading directive result](./images/abp-loading.png) diff --git a/docs/en/UI/Angular/images/abp-loading.png b/docs/en/UI/Angular/images/abp-loading.png new file mode 100644 index 0000000000..50073f6d63 Binary files /dev/null and b/docs/en/UI/Angular/images/abp-loading.png differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index a51d5c248d..1526e884c9 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -1028,6 +1028,10 @@ "text": "Confirmation Popup", "path": "UI/Angular/Confirmation-Service.md" }, + { + "text": "Loading Directive", + "path": "UI/Angular/Loading-Directive.md" + }, { "text": "Toast Overlay", "path": "UI/Angular/Toaster-Service.md" @@ -1289,6 +1293,10 @@ { "text": "IdentityServer", "path": "Modules/IdentityServer.md" + }, + { + "text": "OpenIddict", + "path": "Modules/OpenIddict.md" }, { "text": "Permission Management", diff --git a/docs/zh-Hans/Application-Services.md b/docs/zh-Hans/Application-Services.md index e68217a94d..6d358c3b52 100644 --- a/docs/zh-Hans/Application-Services.md +++ b/docs/zh-Hans/Application-Services.md @@ -87,7 +87,7 @@ public class CreateBookDto } ``` -有关DTO更的教程,请参见[数据传输对象文档](Entities.md) +有关DTO更多的教程,请参见[数据传输对象文档](Entities.md) ### BookAppService(实现) @@ -330,7 +330,7 @@ public class BookAppService : } ```` -`CrudAppService`实现了`ICrudAppService`接口中声明的所有方法. 然后,你可以添加自己的自定义方法或覆盖和自定义实现. +`CrudAppService`实现了`ICrudAppService`接口中声明的所有方法. 然后,你可以添加自己的自定义方法或重写和自定义实现. > `CrudAppService` 有不同数量泛型参数的版本,你可以选择适合的使用. @@ -380,4 +380,4 @@ public class DistrictKey ### 生命周期 -应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. \ No newline at end of file +应用服务的生命周期是[transient](Dependency-Injection)的,它们会自动注册到依赖注入系统. diff --git a/docs/zh-Hans/CLI-New-Command-Samples.md b/docs/zh-Hans/CLI-New-Command-Samples.md new file mode 100644 index 0000000000..3adfa472ed --- /dev/null +++ b/docs/zh-Hans/CLI-New-Command-Samples.md @@ -0,0 +1,248 @@ +# ABP CLI - 新解决方案命令示例 + +`abp new`命令基于abp模板创建abp解决方案或其他组件. [ABP CLI](CLI.md)有一些参数可以用于创建新的ABP解决方案. 在本文档中, 我们将向你展示一些创建新的解决方案的命令示例. 所有的项目名称都是`Acme.BookStore`. 目前, 唯一可用的移动端项目是`React Native`移动端应用程序. 可用的数据库提供程序有`Entity Framework Core`和`MongoDB`. 所有命令都以`abp new`开头. + +## Angular + +以下命令用于创建Angular UI项目: + +* 在新文件夹中创建项目, **Entity Framework Core**, 非移动端应用程序: + + ````bash + abp new Acme.BookStore -u angular --mobile none --database-provider ef -csf + ```` + +* 在新文件夹中创建项目, **Entity Framework Core**, 默认应用程序模板, **拆分Identity Server**: + + ```bash + abp new Acme.BookStore -t app -u angular -m none --separate-identity-server --database-provider ef -csf + ``` + +* 在新文件夹中创建项目, **Entity Framework Core**, **自定义连接字符串**: + + ```bash + abp new Acme.BookStore -u angular -csf --connection-string Server=localhost;Database=MyDatabase;Trusted_Connection=True + ``` + +* 在`C:\MyProjects\Acme.BookStore`中创建解决方案, **MongoDB**, 默认应用程序模板, 包含移动端项目: + + ```bash + abp new Acme.BookStore -u angular --database-provider mongodb --output-folder C:\MyProjects\Acme.BookStore + ``` + +* 在新文件夹中创建项目, **MongoDB**, 默认应用程序模板, 不创建移动端应用程序, **拆分Identity Server**: + + ```bash + abp new Acme.BookStore -t app -u angular -m none --separate-identity-server --database-provider mongodb -csf + ``` + +## MVC + +以下命令用于创建MVC UI项目: + +* 在新文件夹中创建项目, **Entity Framework Core**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef -csf + ``` + +* 在新文件夹中创建项目, **Entity Framework Core**, **分层结构** (*Web和HTTP API层是分开的*), 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u mvc --mobile none --tiered --database-provider ef -csf + ``` + +* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider mongodb -csf + ``` + +* 在新文件夹中创建项目, **MongoDB**, **分层结构**: + + ```bash + abp new Acme.BookStore -u mvc --tiered --database-provider mongodb -csf + ``` + + +## Blazor + +以下命令用于创建Blazor项目: + +* **Entity Framework Core**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u blazor --mobile none + ``` + +* **Entity Framework Core**, **拆分Identity Server**, 包含移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor --separate-identity-server + ``` + +* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf + ``` + +## Blazor Server + +以下命令用于创建Blazor项目: + +* **Entity Framework Core**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -t app -u blazor-server --mobile none + ``` + +* **Entity Framework Core**, **拆分Identity Server**, **拆分API Host**, 包含移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor-server --tiered + ``` + +* 在新文件夹中创建项目, **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u blazor --database-provider mongodb --mobile none -csf + ``` + +## 无UI + +在默认应用程序模板中, 始终有一个前端项目. 在这个选项中没有前端项目. 它有一个`HttpApi.Host`项目为你的HTTP WebAPI提供服务. 这个选项适合在你想创建一个WebAPI服务时使用. + +* 在新文件夹中创建项目, **Entity Framework Core**, 拆分Identity Server: + + ```bash + abp new Acme.BookStore -u none --separate-identity-server -csf + ``` +* **MongoDB**, 不创建移动端应用程序: + + ```bash + abp new Acme.BookStore -u none --mobile none --database-provider mongodb + ``` + + + +## 控制台应用程序 + +这是一个基于.NET控制台应用程序的模板, 集成了ABP模块架构. 要创建控制台应用程序, 请使用以下命令: + +* 项目由以下文件组成: `Acme.BookStore.csproj`, `appsettings.json`, `BookStoreHostedService.cs`, `BookStoreModule.cs`, `HelloWorldService.cs` 和 `Program.cs`. + + ```bash + abp new Acme.BookStore -t console -csf + ``` + +## 模块 + +模块是主项目使用的可重用子应用程序. 如果你正在构建微服务解决方案, 使用ABP模块是最佳方案. 由于模块不是最终的应用程序, 每个模块都有前端UI项目和数据库提供程序. 模块模板带有MVC UI, 可以在没有最终解决方案的情况下进行开发. 但是, 如果要在最终解决方案下开发模块, 可以添加`--no-ui`参数来去除MVC UI项目. + +* 包含前端: `MVC`, `Angular`, `Blazor`. 包含数据库提供程序: `Entity Framework Core`, `MongoDB`. 包含MVC启动项目. + + ```bash + abp new Acme.IssueManagement -t module + ``` +* 与上面相同, 但不包括MVC启动项目. + + ```bash + abp new Acme.IssueManagement -t module --no-ui + ``` + +* 创建模块并将其添加到解决方案中 + + ```bash + abp new Acme.IssueManagement -t module --add-to-solution-file + ``` + +## 从特定版本创建解决方案 + +创建解决方案时, 它总是使用最新版本创建. 要从旧版本创建项目, 可以使用`--version`参数. + +* 使用v3.3.0版本创建解决方案, 包含Angular UI和Entity Framework Core. + + ```bash + abp new Acme.BookStore -t app -u angular -m none --database-provider ef -csf --version 3.3.0 + ``` + +要获取ABP版本列表, 请查看以下链接: https://www.nuget.org/packages/Volo.Abp.Core/ + +## 从自定义模板创建 + +ABP CLI使用默认的[应用程序模板](https://github.com/abpframework/abp/tree/dev/templates/app)创建项目. 如果要从自定义模板创建新的解决方案, 可以使用参数`--template-source`. + +* 在`c:\MyProjects\templates\app`目录中使用模板, MVC UI, Entity Framework Core, 不创建移动端应用程序. + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source "c:\MyProjects\templates\app" + ``` + +* 除了此命令从URL `https://myabp.com/app-template.zip` 检索模板之外, 与上一个命令相同. + + ```bash + abp new Acme.BookStore -t app -u mvc --mobile none --database-provider ef --template-source https://myabp.com/app-template.zip + ``` + +## 创建预览版本 + +ABP CLI始终使用最新版本. 要从预览(RC)版本创建解决方案, 请添加`--preview`参数. + +* 在新文件夹中创建项目, Blazor UI, Entity Framework Core, 不创建移动端应用程序, **使用最新版本**: + + ```bash + abp new Acme.BookStore -t app -u blazor --mobile none -csf --preview + ``` + +## 选择数据库管理系统 + +默认的数据库管理系统是 `Entity Framework Core` / ` SQL Server`. 你可以通过使用`--database-management-system`参数选择DBMS. [可用的值](https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/DatabaseManagementSystem.cs) 包括 `SqlServer`, `MySQL`, `SQLite`, `Oracle`, `Oracle-Devart`, `PostgreSQL`. 默认值是 `SqlServer`. + +* 在新文件夹中创建项目, Angular UI, **PostgreSQL** 数据库: + + ```bash + abp new Acme.BookStore -u angular --database-management-system PostgreSQL -csf + ``` + +## 使用静态HTTP端口 + +ABP CLI始终为项目分配随机端口. 如果需要保留默认端口并且创建解决方案始终使用相同的HTTP端口, 请添加参数`--no-random-port`. + +* 在新文件夹中创建项目, MVC UI, Entity Framework Core, **静态端口**: + + ```bash + abp new Acme.BookStore --no-random-port -csf + ``` + +## 引用本地ABP框架 + +在ABP解决方案中, 默认情况下从NuGet引用ABP库. 有时, 你需要在本地将ABP库引用到你的解决方案中. 这利于调试框架本身. 本地ABP框架的根目录必须有`Volo.Abp.sln`文件. 你可以将以下目录的内容复制到你的文件系统中 + +* MVC UI, Entity Framework Core, **引用本地的ABP库**: + +本地路径必须是ABP存储库的根目录. +如果`C:\source\abp\framework\Volo.Abp.sln`是你的框架解决方案的路径, 那么你必须设置`--abp-path`参数值为`C:\source\abp`. + + ```bash + abp new Acme.BookStore --local-framework-ref --abp-path C:\source\abp + ``` + +**输出**: + +如下所示, 引用本地ABP框架库项目. + +```xml + + + + + + + + +``` + +## 另请参阅 + +* [ABP CLI文档](CLI.md) diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 5ff00c5501..bef5a7220e 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -78,6 +78,8 @@ abp new Acme.BookStore * Acme.BookStore是解决方案的名称. * 常见的命名方式类似于 *YourCompany.YourProject*. 不过你可以使用自己喜欢的方式,如 *YourProject* (单级命名空间) 或 *YourCompany.YourProduct.YourModule* (三级命名空间). +参阅[ABP CLI 创建新解决方案示例](CLI-New-Command-Samples.md)查看更多示例. + #### Options * `--template` 或者 `-t`: 指定模板. 默认的模板是 `app`,会生成web项目.可用的模板有: @@ -407,4 +409,4 @@ abp install-libs [options] #### Options -* ```--working-directory``` 或 ```-wd```: 指定工作目录, 当执行目录不包含项目文件时会很有用. \ No newline at end of file +* ```--working-directory``` 或 ```-wd```: 指定工作目录, 当执行目录不包含项目文件时会很有用. diff --git a/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md b/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md index 1100a22b22..95bce60bfc 100644 --- a/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md +++ b/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md @@ -32,11 +32,11 @@ 领域驱动设计(DDD)是一种将实现与**持续进化**的模型连接在一起来满足**复杂**需求的软件开发方法. -DDD适用于**复杂领域**或**较大规模**的系统,而不是简单的CRUD程序.它着重与**核心领域逻辑**,而不是基础架构.这样有助于构建一个**灵活**,模块化,**可维护**的代码库. +DDD适用于**复杂领域**或**较大规模**的系统,而不是简单的CRUD程序.它着重于**核心领域逻辑**,而不是基础架构.这样有助于构建一个**灵活**,模块化,**可维护**的代码库. ### OOP & SOLID -实现DDD高度依赖面对对象编程思想(OOP)和[SOLID](https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1))原则.事实上,DDD已经**实现**并**延伸**了这些原则,因此,**深入了解**OOP和SOLID对实施DDD十分有利. +实现DDD高度依赖面向对象编程思想(OOP)和[SOLID](https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1))原则.事实上,DDD已经**实现**并**延伸**了这些原则,因此,**深入了解**OOP和SOLID对实施DDD十分有利. ### DDD分层与整洁架构 @@ -154,7 +154,7 @@ ABP的启动解决方案中包含两个用于集成Entity Framework Core的项 * `Application` 依赖`Application.Contracts`项目,因为此项目需要实现应用服务的接口及接口使用的DTO.另外也依赖`Domain`项目,因为应用服务的实现必须依赖领域层中的对象. * `EntityFrameworkCore` 依赖`Domain`项目,因为此项目需要将领域对象(实体或值对象)映射到数据库的表,另外还需要实现`Domain`项目中的仓储接口. * `HttpApi` 依赖`Application.Contracts`项目,因为Controllers需要注入应用服务. -* `HttpApi.Client` 依赖`Application.Contracts`项目,因为此项目需要是使用应用服务. +* `HttpApi.Client` 依赖`Application.Contracts`项目,因为此项目需要使用应用服务. * `Web` 依赖`HttpApi`项目,因为此项目对外提供HTTP APIs.另外Pages或Components 需要使用应用服务,所以还间接依赖了`Application.Contracts`项目 #### 虚线依赖 @@ -198,7 +198,7 @@ ABP的启动解决方案中包含两个用于集成Entity Framework Core的项 ##### 关于数据库独立原则的讨论 -**原因1**会非常影响你**领域对象的建模**(特别是实体间的关系)及**应用程序的代码**.假如,开始选择了关系型数据库,并使用了[Entity Framework Core](Entity-Framework-Core.md),后面尝试切换到[MongoDB](MongoDB.md),那么 **EF Core 中一些非常用的特性**你就不能使用了,例如: +**原因1**会非常影响你**领域对象的建模**(特别是实体间的关系)及**应用程序的代码**.假如,开始选择了关系型数据库,并使用了[Entity Framework Core](Entity-Framework-Core.md),后面尝试切换到[MongoDB](MongoDB.md),那么 **EF Core 中一些非常有用的特性**你就不能使用了,例如: * 无法使用[变更追踪](https://docs.microsoft.com/zh-cn/ef/core/querying/tracking) ,因为*MongoDB provider*没有提供此功能,因此,你始终需要显式的更新已变更的实体. * 无法在不同的聚合间使用[导航属性](https://docs.microsoft.com/zh-cn/ef/core/modeling/relationships),因为文档型数据库是不支持的.有关更多信息,请参见"规则:聚合间仅通过Id关联". @@ -209,7 +209,7 @@ ABP的启动解决方案中包含两个用于集成Entity Framework Core的项 #### 展现层技术无关原则 -展现层技术(UI框架)时现代应用程序中最多变的部分之一.**领域层和应用层**应该对展现层所采用的技术或框架**一无所知**.使用ABP启动模板就非常容易实现此原则. +展现层技术(UI框架)是现代应用程序中最多变的部分之一.**领域层和应用层**应该对展现层所采用的技术或框架**一无所知**.使用ABP启动模板就非常容易实现此原则. 在某些情况下,你可能需要在应用层和展现层中写重复的逻辑,例如,参数验证和授权检查.展现层检查出于**用户体验**,应用层或领域层检查出于**数据安全性**和**数据完整性**. @@ -259,7 +259,7 @@ DDD忽略**领域对象的数据展示**,这并不意味着它们并不重要, 这样做的原因是我们需要执行业务规则来保证数据的一致性和完整性.假如有一个业务规则:"用户不能对已锁定的问题进行评论".那如何在不查询数据库的情况下,获取问题是否已被锁定?所以,只有关联的对象都被加载了的时候,我们才可以执行业务规则. -另外,使用**MongoDB**的开发人员就认为此原则很好理解.在MongoDB中,聚合对象(包含子集合)会被保存到一个`collection`中.因而,无需任何其它配置,就可以实现查询一个聚合,同时所有子对象. +另外,使用**MongoDB**的开发人员就认为此原则很好理解.在MongoDB中,聚合对象(包含子集合)会被保存到一个`collection`中.因而,无需任何其它配置,就可以实现查询一个聚合,同时包含所有子对象. ABP框架有助于你实现这一原则 @@ -293,7 +293,7 @@ public class IssueAppService : ApplicationService, IIssueAppService 最后,我们使用`_issueRepository.UpdateAsync`方法,将对象保存到数据库中. -> EF Core 具有**变更追踪**的功能,因此,不需要调用`_issueRepository.UpdateAsync`方法.ABP的工作单元会再方法结束时,自动执行`DbContext.SaveChanges()`的.如果使用MongoDB则需要显式手动调用. +> EF Core 具有**变更追踪**的功能,因此,不需要调用`_issueRepository.UpdateAsync`方法.ABP的工作单元会在方法结束时,自动执行`DbContext.SaveChanges()`的.如果使用MongoDB则需要显式手动调用. > > 因此,当需要额外编写仓储层的实现,应该在实体变化时始终调用 `UpdateAsync` 方法. @@ -375,7 +375,7 @@ MongoDB中不适合使用导航属性或集合的,原因是:当前源聚合对 并不是所有的子集合的主键都是联合主键,有些情况下,可以使用单独的`Id`作为主键. -> 联合主键实际上时关系型数据库中的概念,因为子集合对象有与之对应的数据库表,而表也要有主键.但是在非关系型数据库中,无需为子集合实体定义主键,因为它们本身就已属于一个聚合根. +> 联合主键实际上是关系型数据库中的概念,因为子集合对象有与之对应的数据库表,而表也要有主键.但是在非关系型数据库中,无需为子集合实体定义主键,因为它们本身就已属于一个聚合根. ##### 聚合根 / 实体的构造函数 @@ -1017,7 +1017,7 @@ public class IssueAppService : ApplicationService, IIssueAppService ### 领域服务 -领域服务主要来实现本领域的逻辑: +领域服务主要用来实现本领域的逻辑: * 依赖**服务和仓储**. * 需要使用多个聚合. @@ -1276,7 +1276,7 @@ public class UserChangePasswordDto 虽然编写了更多的代码,但是这样可维护性更高. -**例外情况:**该规则有一些例外的情况,例如,你想开发两个方法,它们共用相同的输入DTO(通过继承或重用),有一个报表页面有多个过滤条件,多个应用服务使用相同的输入参数返回不同的结果(如,大屏展示数据,Excel报表,csv报表).这种情况下,你是需要修改一个参数,多个应用服务都应该一起被修改. +**例外情况:** 该规则有一些例外的情况,例如,你想开发两个方法,它们共用相同的输入DTO(通过继承或重用),有一个报表页面有多个过滤条件,多个应用服务使用相同的输入参数返回不同的结果(如,大屏展示数据,Excel报表,csv报表).这种情况下,你是需要修改一个参数,多个应用服务都应该一起被修改. ##### 输入DTO中验证逻辑 @@ -1635,7 +1635,7 @@ public class IssueCreationDto 因为,应用服务可能在保存`Issue`对象之前,需要对其它对象进行修改.如果领域服务执行了保存,那么*保存*操作就是重复的. -* 会触发两次数据库会交互,这会导致性能损失. +* 会触发两次数据库交互,这会导致性能损失. * 需要额外添加显式的事务来包含这两个操作,才能保证数据一致性. * 如果因为业务规则取消了实体的创建,则应该在数据库事务中回滚事务,取消所有操作. @@ -1966,7 +1966,7 @@ public class IssueAppService * 如果没有**任何业务逻辑**,只有简单的**CRUD**操作,**请勿**创建领域服务. * **切勿**将**DTO**传递给领域服务,或从领域服务返回**DTO**. -可以在应用服务中直接注入仓储,实现查询,创建,更新及删除操作.除非在这些操作过程中需要执行某些业务逻辑,在这种情况下,请创建领域服务. +可以在应用服务中直接注入仓储,实现查询,创建,更新及删除操作.除非在这些操作过程中需要执行某些领域逻辑,在这种情况下,请创建领域服务. > 不要创建"将来可能需要"这种CRUD领域服务方法([YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it)),在需要时重构它并重构现有代码. 由于应用层优雅地抽象了领域层,因此重构过程不会影响UI层和其他客户端. diff --git a/docs/zh-Hans/Domain-Services.md b/docs/zh-Hans/Domain-Services.md index a617aea645..0c61dd2330 100644 --- a/docs/zh-Hans/Domain-Services.md +++ b/docs/zh-Hans/Domain-Services.md @@ -116,7 +116,6 @@ namespace MyProject.Issues ## 应用程序服务与领域服务 -虽然应用服务和领域服务都实现了业务规则,但存在根本的逻辑和形式差异; 虽然 [应用服务](Application-Services.md) 和领域服务都实现了业务规则,但存在根本的逻辑和形式差异: * 应用程序服务实现应用程序的 **用例** (典型 Web 应用程序中的用户交互), 而领域服务实现 **核心的、用例独立的领域逻辑**. diff --git a/docs/zh-Hans/Entities.md b/docs/zh-Hans/Entities.md index e54258d61f..8fbf16ddd3 100644 --- a/docs/zh-Hans/Entities.md +++ b/docs/zh-Hans/Entities.md @@ -28,7 +28,7 @@ public class Book : Entity * 创建一个构造函数,获取ID作为参数传递给基类. * 如果没有为GUID Id赋值,**ABP框架会在保存时设置它**,但是在将实体保存到数据库之前最好在实体上有一个有效的Id. * 如果使用带参数的构造函数创建实体,那么还要创建一个 `private` 或 `protected` 构造函数. 当数据库提供程序从数据库读取你的实体时(反序列化时)将使用它. -* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中**使用[`IGuidGenerator`服务](Guid-Generation.md)**传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要. +* 不要使用 `Guid.NewGuid()` 来设置Id! 在创建实体的代码中**使用[`IGuidGenerator`服务](Guid-Generation.md)** 传递Id参数. `IGuidGenerator`经过优化可以产生连续的GUID.这对于关系数据库中的聚集索引非常重要. 示例实体: @@ -70,7 +70,7 @@ public class BookAppService : ApplicationService, IBookAppService } ```` -* `BookAppService` 注入图书实体的默认[仓库](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中. +* `BookAppService` 注入图书实体的默认[仓储](Repositories.md),使用`InsertAsync`方法插入 `Book` 到数据库中. * `GuidGenerator`类型是 `IGuidGenerator`,它是在`ApplicationService`基类中定义的属性. ABP将这样常用属性预注入,所以不需要手动[注入](Dependency-Injection.md). * 如果你想遵循DDD最佳实践,请参阅下面的*聚合示例*部分. @@ -373,7 +373,7 @@ public static class IdentityUserExtensions * 对于 [Entity Framework Core](Entity-Framework-Core.md),这是两种类型的配置; * 默认它以 `JSON` 字符串形式存储在 `ExtraProperties` 字段中. 序列化到 `JSON` 和反序列化到 `JSON` 由ABP使用EF Core的[值转换](https://docs.microsoft.com/zh-cn/ef/core/modeling/value-conversions)系统自动完成. - * 如果需要,你可以使用 `ObjectExtensionManager` 为所需的额外属性定义一个单独的数据库字段. 那些使用 `ObjectExtensionManager` 配置的属性继续使用单个 `JSON` 字段. 当你使用预构建的[应用模块](Modules/Index.md)并且想要[扩展模块的实体](Customizing-Application-Modules-Extending-Entities.md). 参阅[EF Core迁移文档](Entity-Framework-Core.md)了解如何使用 `ObjectExtensionManager`. + * 如果需要,你可以使用 `ObjectExtensionManager` 为所需的额外属性定义一个单独的数据库字段. 未使用 `ObjectExtensionManager` 配置的属性继续使用单个 `JSON` 字段. 当你使用预构建的[应用模块](Modules/Index.md)并且想要[扩展模块的实体](Customizing-Application-Modules-Extending-Entities.md). 参阅[EF Core迁移文档](Entity-Framework-Core.md)了解如何使用 `ObjectExtensionManager`. * 对于 [MongoDB](MongoDB.md), 它以 **常规字段** 存储, 因为 MongoDB 天生支持这种 [额外](https://mongodb.github.io/mongo-csharp-driver/1.11/serialization/#supporting-extra-elements) 系统. ### 讨论额外的属性 diff --git a/docs/zh-Hans/Text-Templating-Razor.md b/docs/zh-Hans/Text-Templating-Razor.md new file mode 100644 index 0000000000..7394133587 --- /dev/null +++ b/docs/zh-Hans/Text-Templating-Razor.md @@ -0,0 +1,570 @@ +# Razor 集成 + +Razor模板是标准的C#类, 所以你可以使用任何C#的功能, 例如`依赖注入`, 使用`LINQ`, 自定义方法甚至使用`仓储` + +## 安装 + +建议使用[ABP CLI](CLI.md)安装此包. + +### 使用ABP CLI + +在项目文件夹(.csproj 文件)中打开命令行窗口并输入以下命令: + +````bash +abp add-package Volo.Abp.TextTemplating.Razor +```` + +### 手动安装 + +如果你想要手动安装: + +1. 添加 [Volo.Abp.TextTemplating.Razor](https://www.nuget.org/packages/Volo.Abp.TextTemplating.Razor) NuGet 包到你的项目: + +```` +Install-Package Volo.Abp.TextTemplating.Razor +```` + +2.添加 `AbpTextTemplatingRazorModule` 到你的模块的依赖列表: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpTextTemplatingRazorModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +```` + +## 添加 MetadataReference到CSharpCompilerOptions + +你需要将添加`MetadataReference`模板中使用的类型添加到 `CSharpCompilerOptions` 的 `References`. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.References.Add(MetadataReference.CreateFromFile(typeof(YourModule).Assembly.Location)); + }); +} +``` + +## 添加MetadataReference到模板 + +你可以添加一些`MetadataReference`到模板 + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + services.Configure(options => + { + //Hello is template name. + options.TemplateReferences.Add("Hello", new List() + { + Assembly.Load("Microsoft.Extensions.Logging.Abstractions"), + Assembly.Load("Microsoft.Extensions.Logging") + } + .Select(x => MetadataReference.CreateFromFile(x.Location)) + .ToList()); + }); +} +``` + +## 定义模板 + +在渲染模板之前,需要定义它. 创建一个继承自 `TemplateDefinitionProvider` 的类: + +````csharp +public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider +{ + public override void Define(ITemplateDefinitionContext context) + { + context.Add( + new TemplateDefinition("Hello") //template name: "Hello" + .WithRazorEngine() + .WithVirtualFilePath( + "/Demos/Hello/Hello.cshtml", //template content path + isInlineLocalized: true + ) + ); + } +} +```` + +* `context` 对象用于添加新模板或获取依赖模块定义的模板. 使用 `context.Add(...)` 定义新模板. +* `TemplateDefinition` 是代表模板的类,每个模板必须有唯一的名称(在渲染模板时使用). +* `/Demos/Hello/Hello.cshtml` 是模板文件的路径. +* `isInlineLocalized` 声明针对所有语言使用一个模板(`true` 还是针对每种语言使用不同的模板(`false`). 更多内容参阅下面的本地化部分. +* `WithRenderEngine` 方法为模板设置渲染引擎. + +### 模板基类 + +每个 `cshtml` 模板页面都需要继承`RazorTemplatePageBase` 或 `RazorTemplatePageBase`. 基类提供了一些使用实用的属性可以在模板中使用. 例如: `Localizer`, `ServiceProvider`. + +### 模板内容 + +`WithVirtualFilePath` 表示我们使用[虚拟文件系统](Virtual-File-System.md)存储模板内容. 在项目内创建一个 `Hello.cshtml` 文件,并在属性窗口中将其标记为"**嵌入式资源**": + +![hello-template-razor](images/hello-template-razor.png) + +示例 `Hello.cshtml` 内容如下所示: + +```csharp +namespace HelloModelNamespace +{ + public class HelloModel + { + public string Name { get; set; } + } +} +``` + +[虚拟文件系统](Virtual-File-System.md) 需要在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法添加你的文件: + +````csharp +Configure(options => +{ + options.FileSets.AddEmbedded("TextTemplateDemo"); +}); +```` + +* `TextTemplateDemoModule`是模块类. +* `TextTemplateDemo` 是你的项目的根命名空间. + +## 渲染模板 + +`ITemplateRenderer` 服务用于渲染模板内容. + +### 示例: 渲染一个简单的模板 + +````csharp +public class HelloDemo : ITransientDependency +{ + private readonly ITemplateRenderer _templateRenderer; + + public HelloDemo(ITemplateRenderer templateRenderer) + { + _templateRenderer = templateRenderer; + } + + public async Task RunAsync() + { + var result = await _templateRenderer.RenderAsync( + "Hello", //the template name + new HelloModel + { + Name = "John" + } + ); + + Console.WriteLine(result); + } +} +```` + +* `HelloDemo` 是一个简单的类,在构造函数注入了 `ITemplateRenderer` 并在 `RunAsync` 方法中使用它. +* `RenderAsync` 有两个基本参数: + * `templateName`: 要渲染的模板名称 (本示例中是 `Hello`). + * `model`: 在模板内部用做 `model` 的对象 (本示例中是 `HelloModel` 对象). + +示例会返回以下结果: + +````csharp +Hello John :) +```` + +## 本地化 + +可以基于当前文化对模板内容进行本地化. 以下部分描述了两种类型的本地化选项. + +### 内联本地化 + +内联本地化使用[本地化系统](Localization.md)本地化模板内的文本. + +#### 示例: 重置密码链接 + +假设你需要向用户发送电子邮件重置密码. 模板内容: + +```csharp +namespace ResetMyPasswordModelNamespace +{ + public class ResetMyPasswordModel + { + public string Link { get; set; } + + public string Name { get; set; } + } +} +``` + +```csharp +@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase +@Localizer["ResetMyPassword", Model.Name] +``` + +`Localizer` 函数用于根据当前用户的文化来定位给定的Key,你需要在本地化文件中定义 `ResetMyPassword` 键: + +````json +"ResetMyPasswordTitle": "Reset my password", +"ResetMyPassword": "Hi {0}, Click here to reset your password" +```` + +你还需要在模板定义提供程序类中声明要与此模板一起使用的本地化资源: + +````csharp +context.Add( + new TemplateDefinition( + "PasswordReset", //Template name + typeof(DemoResource) //LOCALIZATION RESOURCE + ) + .WithRazorEngine() + .WithVirtualFilePath( + "/Demos/PasswordReset/PasswordReset.cshtml", //template content path + isInlineLocalized: true + ) +); +```` + +当你这样渲染模板时: + +````csharp +var result = await _templateRenderer.RenderAsync( + "PasswordReset", //the template name + new PasswordResetModel + { + Name = "john", + Link = "https://abp.io/example-link?userId=123&token=ABC" + } +); +```` + +你可以看到以下本地化结果: + +````csharp +Hi john, Click here to reset your password +```` + +> 如果你为应用程序定义了 [默认本地化资源](Localization.md), 则无需声明模板定义的资源类型. + +### 多个内容本地化 + +你可能希望为每种语言创建不同的模板文件,而不是使用本地化系统本地化单个模板. 如果模板对于特定的文化(而不是简单的文本本地化)应该是完全不同的,则可能需要使用它. + +#### 示例: 欢迎电子邮件模板 + +假设你要发送电子邮件欢迎用户,但要定义基于用户的文化完全不同的模板. + +首先创建一个文件夹,将模板放在里面,像 `en.cshtml`, `tr.cshtml` 每一个你支持的文化: + +![multiple-file-template-razor](images/multiple-file-template-razor.png) + +然后在模板定义提供程序类中添加模板定义: + +````csharp +context.Add( + new TemplateDefinition( + name: "WelcomeEmail", + defaultCultureName: "en" + ) + .WithRazorEngine() + .WithVirtualFilePath( + "/Demos/WelcomeEmail/Templates", //template content folder + isInlineLocalized: false + ) +); +```` + +* 设置 **默认文化名称**, 当没有所需的文化模板,回退到缺省文化. +* 指定 **模板文件夹** 而不是单个模板文件. +* 设置 `isInlineLocalized` 为 `false`. + +就这些,你可以渲染当前文化的模板: + +````csharp +var result = await _templateRenderer.RenderAsync("WelcomeEmail"); +```` + +> 为了简单我们跳过了模型,但是你可以使用前面所述的模型. + +### 指定文化 + +`ITemplateRenderer` 服务如果没有指定则使用当前文化 (`CultureInfo.CurrentUICulture`). 如果你需要你可以使用 `cultureName` 参数指定文化. + +````csharp +var result = await _templateRenderer.RenderAsync( + "WelcomeEmail", + cultureName: "en" +); +```` + +## 布局模板 + +布局模板用于在其他模板之间创建共享布局. 它类似于ASP.NET Core MVC / Razor Pages中的布局系统. + +### 示例: 邮件HTML布局模板 + +例如,你想为所有电子邮件模板创建一个布局. + +首先像之前一样创建一个模板文件: + +```csharp +@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase + + + + + + + @Body + + +``` + +* 布局模板必须具有 **Body** 部分作为渲染的子内容的占位符. + +在模板定义提供程序中注册模板: + +````csharp +context.Add( + new TemplateDefinition( + "EmailLayout", + isLayout: true //SET isLayout! + ) + .WithRazorEngine() + .WithVirtualFilePath( + "/Demos/EmailLayout/EmailLayout.cshtml", + isInlineLocalized: true + ) +); +```` + +现在你可以将此模板用作任何其他模板的布局: + +````csharp +context.Add( + new TemplateDefinition( + name: "WelcomeEmail", + defaultCultureName: "en", + layout: "EmailLayout" //Set the LAYOUT + ) + .WithRazorEngine() + .WithVirtualFilePath( + "/Demos/WelcomeEmail/Templates", + isInlineLocalized: false + ) +); +```` + +## 全局上下文 + +ABP传递 `model`,可用于访问模板内的模型. 如果需要,可以传递更多的全局变量. + +示例模板内容: + +````csharp +@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase +A global object value: @GlobalContext["myGlobalObject"] +```` + +模板假定它渲染上下文中的 `myGlobalObject` 对象. 你可以如下所示提供它: + +````csharp +var result = await _templateRenderer.RenderAsync( + "GlobalContextUsage", + globalContext: new Dictionary + { + {"myGlobalObject", "TEST VALUE"} + } +); +```` + +渲染的结果将是: + +```` +A global object value: TEST VALUE +```` + +## 替换存在的模板 + +通过替换应用程序中使用的模块定义的模板. 这样你可以根据自己的需求自定义模板,而无需更改模块代码. + +### 选项-1: 使用虚拟文件系统 + +[虚拟文件系统](Virtual-File-System.md)允许你通过将相同文件放入项目中的相同路径来覆盖任何文件. + +#### 示例: 替换标准电子邮件布局模板 + +ABP框架提供了一个[邮件发送系统](Emailing.md), 它在内部使用文本模板来渲染邮件内容. 它在 `/Volo/Abp/Emailing/Templates/Layout.cshtml` 路径定义了一个标准邮件布局模板. 模板的唯一名称是 `Abp.StandardEmailTemplates.Layout` 并且这个字符中`Volo.Abp.Emailing.Templates.StandardEmailTemplates`静态类上定义为常量. + +执行以下步骤将替换模板替换成你自定义的; + +**1)** 在你的项目中相同的路径添加一个新文件 (`/Volo/Abp/Emailing/Templates/Layout.cshtml`): + +![replace-email-layout-razor](images/replace-email-layout-razor.png) + +**2)** 准备你的邮件布局模板: + +````html +@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase + + + + + + +

This my header

+ + @Body + +
+ This is my footer... +
+ + +```` + +此示例只是向模板添加页眉和页脚并呈现它们之间的内容(请参阅上面的布局模板部分). + + +**3)** 在`.csproj`文件配置嵌入式资源e + +* 添加 [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet 包到你的项目. +* 在 `.csproj` 中添加 `true` 到 `...` 部分. +* 添加以下代码到你的 `.csproj` 文件: + +````xml + + + + +```` + +这将模板文件做为"嵌入式资源". + +**4)** 配置虚拟文件系统 + +在[模块](Module-Development-Basics.md) `ConfigureServices` 方法配置 `AbpVirtualFileSystemOptions` 将嵌入的文件添加到虚拟文件系统中: + +```csharp +Configure(options => +{ + options.FileSets.AddEmbedded(); +}); +``` + +`BookStoreDomainModule` 应该是你的模块名称 + +> 确保你的模块(支持或间接)[依赖](Module-Development-Basics.md) `AbpEmailingModule`. 因为VFS可以基于依赖顺序覆盖文件. + +现在渲染邮件布局模板时会使用你的模板. + +### 选项-2: 使用模板定义提供者 + +你可以创建一个模板定义提供者类来获取邮件布局模板来更改它的虚拟文件路径. + +**示例: 使用 `/MyTemplates/EmailLayout.cshtml` 文件而不是标准模板** + +```csharp +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.TextTemplating; + +namespace MyProject +{ + public class MyTemplateDefinitionProvider + : TemplateDefinitionProvider, ITransientDependency + { + public override void Define(ITemplateDefinitionContext context) + { + var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout); + + emailLayoutTemplate + .WithVirtualFilePath( + "/MyTemplates/EmailLayout.cshtml", + isInlineLocalized: true + ); + } + } +} +``` + +你依然应该添加 `/MyTemplates/EmailLayout.cshtml` 到虚拟文件系统. 这种方法允许你在任何文件夹中找到模板,而不是在依赖模块定义的文件夹中. + +除了模板内容之外你还可以操作模板定义属性, 例如`DisplayName`, `Layout`或`LocalizationSource`. + +## 高级功能 + +本节介绍文本模板系统的一些内部知识和高级用法. + +### 模板内容Provider + +`TemplateRenderer` 用于渲染模板,这是大多数情况下所需的模板. 但是你可以使用 `ITemplateContentProvider` 获取原始(未渲染的)模板内容. + +> `ITemplateRenderer` 内部使用 `ITemplateContentProvider` 获取原始模板内容. + +示例: + +````csharp +public class TemplateContentDemo : ITransientDependency +{ + private readonly ITemplateContentProvider _templateContentProvider; + + public TemplateContentDemo(ITemplateContentProvider templateContentProvider) + { + _templateContentProvider = templateContentProvider; + } + + public async Task RunAsync() + { + var result = await _templateContentProvider + .GetContentOrNullAsync("Hello"); + + Console.WriteLine(result); + } +} +```` + +结果是原始模板内容: + +```` +@inherits Volo.Abp.TextTemplating.Razor.RazorTemplatePageBase +Hello @Model.Name +```` + +* `GetContentOrNullAsync` 如果没有为请求的模板定义任何内容,则返回 `null`. +* 它可以获取 `cultureName` 参数,如果模板针对不同的文化具有不同的文件,则可以使用该参数(请参见上面的"多内容本地化"部分). + +### 模板内容贡献者 + +`ITemplateContentProvider` 服务使用 `ITemplateContentContributor` 实现来查找模板内容. 有一个预实现的内容贡献者 `VirtualFileTemplateContentContributor`,它从上面描述的虚拟文件系统中获取模板内容. + +你可以实现 `ITemplateContentContributor` 从另一个源读取原始模板内容. + +示例: + +````csharp +public class MyTemplateContentProvider + : ITemplateContentContributor, ITransientDependency +{ + public async Task GetOrNullAsync(TemplateContentContributorContext context) + { + var templateName = context.TemplateDefinition.Name; + + //TODO: Try to find content from another source + return null; + } +} +```` + +如果源无法找到内容, 则返回 `null`, `ITemplateContentProvider` 将回退到下一个贡献者. + +### Template Definition Manager + +`ITemplateDefinitionManager` 服务可用于获取模板定义(由模板定义提供程序创建). + +## 另请参阅 + +* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). +* [本地化系统](Localization.md). +* [虚拟文件系统](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/zh-Hans/Text-Templating-Scriban.md b/docs/zh-Hans/Text-Templating-Scriban.md new file mode 100644 index 0000000000..97cd0ab631 --- /dev/null +++ b/docs/zh-Hans/Text-Templating-Scriban.md @@ -0,0 +1,527 @@ +# Scriban 集成 + +## 安装 + +建议使用[ABP CLI](CLI.md)安装此包. + +### 使用ABP CLI + +在项目文件夹(.csproj 文件)中打开命令行窗口并输入以下命令: + +````bash +abp add-package Volo.Abp.TextTemplating.Scriban +```` + +### 手动安装 + +如果你想要手动安装: + +1. 添加 [Volo.Abp.TextTemplating.Scriban](https://www.nuget.org/packages/Volo.Abp.TextTemplating.Scriban) NuGet 包到你的项目: + +```` +Install-Package Volo.Abp.TextTemplating.Scriban +```` + +2.添加 `AbpTextTemplatingScribanModule` 到你的模块的依赖列表: + +````csharp +[DependsOn( + //...other dependencies + typeof(AbpTextTemplatingScribanModule) //Add the new module dependency + )] +public class YourModule : AbpModule +{ +} +```` + +## 定义模板 + +在渲染模板之前,需要定义它. 创建一个继承自 `TemplateDefinitionProvider` 的类: + +````csharp +public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider +{ + public override void Define(ITemplateDefinitionContext context) + { + context.Add( + new TemplateDefinition("Hello") //template name: "Hello" + .WithVirtualFilePath( + "/Demos/Hello/Hello.tpl", //template content path + isInlineLocalized: true + ) + .WithScribanEngine() + ); + } +} +```` + +* `context` 对象用于添加新模板或获取依赖模块定义的模板. 使用 `context.Add(...)` 定义新模板. +* `TemplateDefinition` 是代表模板的类,每个模板必须有唯一的名称(在渲染模板时使用). +* `/Demos/Hello/Hello.tpl` 是模板文件的路径. +* `isInlineLocalized` 声明针对所有语言使用一个模板(`true` 还是针对每种语言使用不同的模板(`false`). 更多内容参阅下面的本地化部分. +* `WithScribanEngine` 方法为模板设置渲染引擎. + +### 模板内容 + +`WithVirtualFilePath` 表示我们使用[虚拟文件系统](Virtual-File-System.md)存储模板内容. 在项目内创建一个 `Hello.tpl` 文件,并在属性窗口中将其标记为"**嵌入式资源**": + +![hello-template](images/hello-template.png) + +示例 `Hello.tpl` 内容如下所示: + +```` +Hello {%{{{model.name}}}%} :) +```` + +[虚拟文件系统](Virtual-File-System.md) 需要在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法添加你的文件: + +````csharp +Configure(options => +{ + options.FileSets.AddEmbedded("TextTemplateDemo"); +}); +```` + +* `TextTemplateDemoModule`是模块类. +* `TextTemplateDemo` 是你的项目的根命名空间. + +## 渲染模板 + +`ITemplateRenderer` 服务用于渲染模板内容. + +### 示例: 渲染一个简单的模板 + +````csharp +public class HelloDemo : ITransientDependency +{ + private readonly ITemplateRenderer _templateRenderer; + + public HelloDemo(ITemplateRenderer templateRenderer) + { + _templateRenderer = templateRenderer; + } + + public async Task RunAsync() + { + var result = await _templateRenderer.RenderAsync( + "Hello", //the template name + new HelloModel + { + Name = "John" + } + ); + + Console.WriteLine(result); + } +} +```` + +* `HelloDemo` 是一个简单的类,在构造函数注入了 `ITemplateRenderer` 并在 `RunAsync` 方法中使用它. +* `RenderAsync` 有两个基本参数: + * `templateName`: 要渲染的模板名称 (本示例中是 `Hello`). + * `model`: 在模板内部用做 `model` 的对象 (本示例中是 `HelloModel` 对象). + +示例会返回以下结果: + +````csharp +Hello John :) +```` + +### 匿名模型 + +虽然建议为模板创建模型类,但在简单情况下使用匿名对象也是可行的: + +````csharp +var result = await _templateRenderer.RenderAsync( + "Hello", + new + { + Name = "John" + } +); +```` + +示例中我们并没有创建模型类,但是创建了一个匿名对象模型. + +### PascalCase 与 snake_case + +PascalCase 属性名(如 `UserName`) 在模板中使用蛇形命名(如 `user_name`). + +## 本地化 + +可以基于当前文化对模板内容进行本地化. 以下部分描述了两种类型的本地化选项. + +### 内联本地化 + +内联本地化使用[本地化系统](Localization.md)本地化模板内的文本. + +#### 示例: 重置密码链接 + +假设你需要向用户发送电子邮件重置密码. 模板内容: + +```` +{%{{{L "ResetMyPassword" model.name}}}%} +```` + +`L` 函数用于根据当前用户的文化来定位给定的Key,你需要在本地化文件中定义 `ResetMyPassword` 键: + +````json +"ResetMyPasswordTitle": "Reset my password", +"ResetMyPassword": "Hi {0}, Click here to reset your password" +```` + +你还需要在模板定义提供程序类中声明要与此模板一起使用的本地化资源: + +````csharp +context.Add( + new TemplateDefinition( + "PasswordReset", //Template name + typeof(DemoResource) //LOCALIZATION RESOURCE + ) + .WithScribanEngine() + .WithVirtualFilePath( + "/Demos/PasswordReset/PasswordReset.tpl", //template content path + isInlineLocalized: true + ) +); +```` + +当你这样渲染模板时: + +````csharp +var result = await _templateRenderer.RenderAsync( + "PasswordReset", //the template name + new PasswordResetModel + { + Name = "john", + Link = "https://abp.io/example-link?userId=123&token=ABC" + } +); +```` + +你可以看到以下本地化结果: + +````csharp +Hi john, Click here to reset your password +```` + +> 如果你为应用程序定义了 [默认本地化资源](Localization.md), 则无需声明模板定义的资源类型. + +### 多个内容本地化 + +你可能希望为每种语言创建不同的模板文件,而不是使用本地化系统本地化单个模板. 如果模板对于特定的文化(而不是简单的文本本地化)应该是完全不同的,则可能需要使用它. + +#### 示例: 欢迎电子邮件模板 + +假设你要发送电子邮件欢迎用户,但要定义基于用户的文化完全不同的模板. + +首先创建一个文件夹,将模板放在里面,像 `en.tpl`, `tr.tpl` 每一个你支持的文化: + +![multiple-file-template](images/multiple-file-template.png) + +然后在模板定义提供程序类中添加模板定义: + +````csharp +context.Add( + new TemplateDefinition( + name: "WelcomeEmail", + defaultCultureName: "en" + ) + .WithScribanEngine() + .WithVirtualFilePath( + "/Demos/WelcomeEmail/Templates", //template content folder + isInlineLocalized: false + ) +); +```` + +* 设置 **默认文化名称**, 当没有所需的文化模板,回退到缺省文化. +* 指定 **模板文件夹** 而不是单个模板文件. +* 设置 `isInlineLocalized` 为 `false`. + +就这些,你可以渲染当前文化的模板: + +````csharp +var result = await _templateRenderer.RenderAsync("WelcomeEmail"); +```` + +> 为了简单我们跳过了模型,但是你可以使用前面所述的模型. + +### 指定文化 + +`ITemplateRenderer` 服务如果没有指定则使用当前文化 (`CultureInfo.CurrentUICulture`). 如果你需要你可以使用 `cultureName` 参数指定文化. + +````csharp +var result = await _templateRenderer.RenderAsync( + "WelcomeEmail", + cultureName: "en" +); +```` + +## 布局模板 + +布局模板用于在其他模板之间创建共享布局. 它类似于ASP.NET Core MVC / Razor Pages中的布局系统. + +### 示例: 邮件HTML布局模板 + +例如,你想为所有电子邮件模板创建一个布局. + +首先像之前一样创建一个模板文件: + +````xml + + + + + + + {%{{{content}}}%} + + +```` + +* 布局模板必须具有 **{%{{{content}}}%}** 部分作为渲染的子内容的占位符. + +在模板定义提供程序中注册模板: + +````csharp +context.Add( + new TemplateDefinition( + "EmailLayout", + isLayout: true //SET isLayout! + ) + .WithScribanEngine() + .WithVirtualFilePath( + "/Demos/EmailLayout/EmailLayout.tpl", + isInlineLocalized: true + ) +); +```` + +现在你可以将此模板用作任何其他模板的布局: + +````csharp +context.Add( + new TemplateDefinition( + name: "WelcomeEmail", + defaultCultureName: "en", + layout: "EmailLayout" //Set the LAYOUT + ) + .WithScribanEngine() + .WithVirtualFilePath( + "/Demos/WelcomeEmail/Templates", + isInlineLocalized: false + ) +); +```` + +## 全局上下文 + +ABP传递 `model`,可用于访问模板内的模型. 如果需要,可以传递更多的全局变量. + +示例模板内容: + +```` +A global object value: {%{{{myGlobalObject}}}%} +```` + +模板假定它渲染上下文中的 `myGlobalObject` 对象. 你可以如下所示提供它: + +````csharp +var result = await _templateRenderer.RenderAsync( + "GlobalContextUsage", + globalContext: new Dictionary + { + {"myGlobalObject", "TEST VALUE"} + } +); +```` + +渲染的结果将是: + +```` +A global object value: TEST VALUE +```` + +## 替换存在的模板 + +通过替换应用程序中使用的模块定义的模板. 这样你可以根据自己的需求自定义模板,而无需更改模块代码. + +### 选项-1: 使用虚拟文件系统 + +[虚拟文件系统](Virtual-File-System.md)允许你通过将相同文件放入项目中的相同路径来覆盖任何文件. + +#### 示例: 替换标准电子邮件布局模板 + +ABP框架提供了一个[邮件发送系统](Emailing.md), 它在内部使用文本模板来渲染邮件内容. 它在 `/Volo/Abp/Emailing/Templates/Layout.tp` 路径定义了一个标准邮件布局模板. 模板的唯一名称是 `Abp.StandardEmailTemplates.Layout` 并且这个字符中`Volo.Abp.Emailing.Templates.StandardEmailTemplates`静态类上定义为常量. + +执行以下步骤将替换模板替换成你自定义的; + +**1)** 在你的项目中相同的路径添加一个新文件 (`/Volo/Abp/Emailing/Templates/Layout.tpl`): + +![replace-email-layout](images/replace-email-layout.png) + +**2)** 准备你的邮件布局模板: + +````html + + + + + + +

This my header

+ + {%{{{content}}}%} + +
+ This is my footer... +
+ + +```` + +此示例只是向模板添加页眉和页脚并呈现它们之间的内容(请参阅上面的布局模板部分). + + +**3)** 在`.csproj`文件配置嵌入式资源e + +* 添加 [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet 包到你的项目. +* 在 `.csproj` 中添加 `true` 到 `...` 部分. +* 添加以下代码到你的 `.csproj` 文件: + +````xml + + + + +```` + +这将模板文件做为"嵌入式资源". + +**4)** 配置虚拟文件系统 + +在[模块](Module-Development-Basics.md) `ConfigureServices` 方法配置 `AbpVirtualFileSystemOptions` 将嵌入的文件添加到虚拟文件系统中: + +```csharp +Configure(options => +{ + options.FileSets.AddEmbedded(); +}); +``` + +`BookStoreDomainModule` 应该是你的模块名称 + +> 确保你的模块(支持或间接)[依赖](Module-Development-Basics.md) `AbpEmailingModule`. 因为VFS可以基于依赖顺序覆盖文件. + +现在渲染邮件布局模板时会使用你的模板. + +### 选项-2: 使用模板定义提供者 + +你可以创建一个模板定义提供者类来获取邮件布局模板来更改它的虚拟文件路径. + +**示例: 使用 `/MyTemplates/EmailLayout.tpl` 文件而不是标准模板** + +```csharp +using Volo.Abp.DependencyInjection; +using Volo.Abp.Emailing.Templates; +using Volo.Abp.TextTemplating; + +namespace MyProject +{ + public class MyTemplateDefinitionProvider + : TemplateDefinitionProvider, ITransientDependency + { + public override void Define(ITemplateDefinitionContext context) + { + var emailLayoutTemplate = context.GetOrNull(StandardEmailTemplates.Layout); + + emailLayoutTemplate + .WithVirtualFilePath( + "/MyTemplates/EmailLayout.tpl", + isInlineLocalized: true + ); + } + } +} +``` + +你依然应该添加 `/MyTemplates/EmailLayout.tpl` 到虚拟文件系统. 这种方法允许你在任何文件夹中找到模板,而不是在依赖模块定义的文件夹中. + +除了模板内容之外你还可以操作模板定义属性, 例如`DisplayName`, `Layout`或`LocalizationSource`. + +## 高级功能 + +本节介绍文本模板系统的一些内部知识和高级用法. + +### 模板内容Provider + +`TemplateRenderer` 用于渲染模板,这是大多数情况下所需的模板. 但是你可以使用 `ITemplateContentProvider` 获取原始(未渲染的)模板内容. + +> `ITemplateRenderer` 内部使用 `ITemplateContentProvider` 获取原始模板内容. + +示例: + +````csharp +public class TemplateContentDemo : ITransientDependency +{ + private readonly ITemplateContentProvider _templateContentProvider; + + public TemplateContentDemo(ITemplateContentProvider templateContentProvider) + { + _templateContentProvider = templateContentProvider; + } + + public async Task RunAsync() + { + var result = await _templateContentProvider + .GetContentOrNullAsync("Hello"); + + Console.WriteLine(result); + } +} +```` + +结果是原始模板内容: + +```` +Hello {%{{{model.name}}}%} :) +```` + +* `GetContentOrNullAsync` 如果没有为请求的模板定义任何内容,则返回 `null`. +* 它可以获取 `cultureName` 参数,如果模板针对不同的文化具有不同的文件,则可以使用该参数(请参见上面的"多内容本地化"部分). + +### 模板内容贡献者 + +`ITemplateContentProvider` 服务使用 `ITemplateContentContributor` 实现来查找模板内容. 有一个预实现的内容贡献者 `VirtualFileTemplateContentContributor`,它从上面描述的虚拟文件系统中获取模板内容. + +你可以实现 `ITemplateContentContributor` 从另一个源读取原始模板内容. + +示例: + +````csharp +public class MyTemplateContentProvider + : ITemplateContentContributor, ITransientDependency +{ + public async Task GetOrNullAsync(TemplateContentContributorContext context) + { + var templateName = context.TemplateDefinition.Name; + + //TODO: Try to find content from another source + return null; + } +} + +```` + +如果源无法找到内容, 则返回 `null`, `ITemplateContentProvider` 将回退到下一个贡献者. + +### Template Definition Manager + +`ITemplateDefinitionManager` 服务可用于获取模板定义(由模板定义提供程序创建). + +## 另请参阅 + +* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). +* [本地化系统](Localization.md). +* [虚拟文件系统](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/zh-Hans/Text-Templating.md b/docs/zh-Hans/Text-Templating.md index d61c85bc9b..4a21c89c1f 100644 --- a/docs/zh-Hans/Text-Templating.md +++ b/docs/zh-Hans/Text-Templating.md @@ -8,450 +8,30 @@ Template + Model =renderer=> Rendered Content 它非常类似于 ASP.NET Core Razor View (或 Page): -*RAZOR VIEW (or PAGE) + MODEL ==render==> HTML CONTENT* +*RAZOR VIEW (或 PAGE) + MODEL ==render==> HTML CONTENT* 你可以将渲染的输出用于任何目的,例如发送电子邮件或准备一些报告. -### 示例 +模板渲染引擎非常强大: -Here, a simple template: - -```` -Hello {%{{{model.name}}}%} :) -```` - -你可以定义一个含有 `Name` 属性的类来渲染这个模板: - -````csharp -public class HelloModel -{ - public string Name { get; set; } -} -```` - -如果你使用 `Name` 为 `John` 的 `HelloModel` 渲染模板,输出为: - -```` -Hello John :) -```` - -模板渲染引擎非常强大; - -* 它基于 [Scriban](https://github.com/lunet-io/scriban) 库, 所以它支持 **条件逻辑**, **循环** 等. -* 模板内容 **可以本地化**. -* 你可以定义 **布局模板** 在渲染其他模板中用做布局. +* 它支持**条件逻辑**, **循环**等等. +* 模板内容**可以本地化**. +* 你可以为其他渲染模板定义**布局模板**。 * 对于高级场景,你可以传递任何对象到模板上下文. -### 源码 - -这里是本文开发和引用的[示例应用程序源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). - -## 安装 - -推荐使用 [ABP CLI](CLI.md) 安装包. - -### 使用 ABP CLI - -在项目目录(.csproj file)打开命令行窗口运行以下命令: - -````bash -abp add-package Volo.Abp.TextTemplating -```` - -### 手动安装 - -如果你想要手动安装; - -1. 添加 [Volo.Abp.TextTemplating](https://www.nuget.org/packages/Volo.Abp.TextTemplating) NuGet包到你的项目: - -```` -Install-Package Volo.Abp.TextTemplating -```` - -2. 添加 `AbpTextTemplatingModule` 到你的模块依赖列表: - -````csharp -[DependsOn( - //...other dependencies - typeof(AbpTextTemplatingModule) //Add the new module dependency - )] -public class YourModule : AbpModule -{ -} -```` - -## 定义模板 - -在渲染模板之前,需要定义它. 创建一个继承自 `TemplateDefinitionProvider` 的类: - -````csharp -public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider -{ - public override void Define(ITemplateDefinitionContext context) - { - context.Add( - new TemplateDefinition("Hello") //template name: "Hello" - .WithVirtualFilePath( - "/Demos/Hello/Hello.tpl", //template content path - isInlineLocalized: true - ) - ); - } -} -```` - -* `context` 对象用于添加新模板或获取依赖模块定义的模板. 使用 `context.Add(...)` 定义新模板. -* `TemplateDefinition` 是代表模板的类,每个模板必须有唯一的名称(在渲染模板时使用). -* `/Demos/Hello/Hello.tpl` 是模板文件的路径. -* `isInlineLocalized` 声明针对所有语言使用一个模板(`true` 还是针对每种语言使用不同的模板(`false`). 更多内容参阅下面的本地化部分. - -### 模板内容 - -`WithVirtualFilePath` 表示我们使用[虚拟文件系统](Virtual-File-System.md)存储模板内容. 在项目内创建一个 `Hello.tpl` 文件,并在属性窗口中将其标记为"**嵌入式资源**": - -![hello-template](images/hello-template.png) - -示例 `Hello.tpl` 内容如下所示: - -```` -Hello {%{{{model.name}}}%} :) -```` - -[虚拟文件系统](Virtual-File-System.md) 需要在[模块](Module-Development-Basics.md)类的 `ConfigureServices` 方法添加你的文件: - -````csharp -Configure(options => -{ - options.FileSets.AddEmbedded("TextTemplateDemo"); -}); -```` - -* `TextTemplateDemoModule`是模块类. -* `TextTemplateDemo` 是你的项目的根命名空间. - -## 渲染模板 - -`ITemplateRenderer` 服务用于渲染模板内容. - -### 示例: 渲染一个简单的模板 - -````csharp -public class HelloDemo : ITransientDependency -{ - private readonly ITemplateRenderer _templateRenderer; - - public HelloDemo(ITemplateRenderer templateRenderer) - { - _templateRenderer = templateRenderer; - } - - public async Task RunAsync() - { - var result = await _templateRenderer.RenderAsync( - "Hello", //the template name - new HelloModel - { - Name = "John" - } - ); - - Console.WriteLine(result); - } -} -```` - -* `HelloDemo` 是一个简单的类,在构造函数注入了 `ITemplateRenderer` 并在 `RunAsync` 方法中使用它. -* `RenderAsync` 有两个基本参数: - * `templateName`: 要渲染的模板名称 (本示例中是 `Hello`). - * `model`: 在模板内部用做 `model` 的对象 (本示例中是 `HelloModel` 对象). - -示例会返回以下结果: - -````csharp -Hello John :) -```` - -### 匿名模型 - -虽然建议为模板创建模型类,但在简单情况下使用匿名对象也是可行的: - -````csharp -var result = await _templateRenderer.RenderAsync( - "Hello", - new - { - Name = "John" - } -); -```` - -示例中我们并没有创建模型类,但是创建了一个匿名对象模型. - -### PascalCase 与 snake_case - -PascalCase 属性名(如 `UserName`) 在模板中使用蛇形命名(如 `user_name`). - -## 本地化 - -可以基于当前文化对模板内容进行本地化. 以下部分描述了两种类型的本地化选项. - -### 内联本地化 - -内联本地化使用[本地化系统](Localization.md)本地化模板内的文本. - -#### 示例: 重置密码链接 - -假设你需要向用户发送电子邮件重置密码. 模板内容: - -```` -{%{{{L "ResetMyPassword" model.name}}}%} -```` - -`L` 函数用于根据当前用户的文化来定位给定的Key,你需要在本地化文件中定义 `ResetMyPassword` 键: - -````json -"ResetMyPasswordTitle": "Reset my password", -"ResetMyPassword": "Hi {0}, Click here to reset your password" -```` - -你还需要在模板定义提供程序类中声明要与此模板一起使用的本地化资源: - -````csharp -context.Add( - new TemplateDefinition( - "PasswordReset", //Template name - typeof(DemoResource) //LOCALIZATION RESOURCE - ).WithVirtualFilePath( - "/Demos/PasswordReset/PasswordReset.tpl", //template content path - isInlineLocalized: true - ) -); -```` - -当你这样渲染模板时: - -````csharp -var result = await _templateRenderer.RenderAsync( - "PasswordReset", //the template name - new PasswordResetModel - { - Name = "john", - Link = "https://abp.io/example-link?userId=123&token=ABC" - } -); -```` - -你可以看到以下本地化结果: - -````csharp -Hi john, Click here to reset your password -```` - -> 如果你为应用程序定义了 [默认本地化资源](Localization.md), 则无需声明模板定义的资源类型. - -### 多个内容本地化 - -你可能希望为每种语言创建不同的模板文件,而不是使用本地化系统本地化单个模板. 如果模板对于特定的文化(而不是简单的文本本地化)应该是完全不同的,则可能需要使用它. - -#### 示例: 欢迎电子邮件模板 - -假设你要发送电子邮件欢迎用户,但要定义基于用户的文化完全不同的模板. - -首先创建一个文件夹,将模板放在里面,像 `en.tpl`, `tr.tpl` 每一个你支持的文化: - -![multiple-file-template](images/multiple-file-template.png) - -然后在模板定义提供程序类中添加模板定义: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en" - ) - .WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", //template content folder - isInlineLocalized: false - ) -); -```` - -* 设置 **默认文化名称**, 当没有所需的文化模板,回退到缺省文化. -* 指定 **模板文件夹** 而不是单个模板文件. -* 设置 `isInlineLocalized` 为 `false`. - -就这些,你可以渲染当前文化的模板: - -````csharp -var result = await _templateRenderer.RenderAsync("WelcomeEmail"); -```` - -> 为了简单我们跳过了模型,但是你可以使用前面所述的模型. - -### 指定文化 - -`ITemplateRenderer` 服务如果没有指定则使用当前文化 (`CultureInfo.CurrentUICulture`). 如果你需要你可以使用 `cultureName` 参数指定文化. - -````csharp -var result = await _templateRenderer.RenderAsync( - "WelcomeEmail", - cultureName: "en" -); -```` - -## 布局模板 - -布局模板用于在其他模板之间创建共享布局. 它类似于ASP.NET Core MVC / Razor Pages中的布局系统. - -### 示例: 邮件HTML布局模板 - -例如,你想为所有电子邮件模板创建一个布局. - -首先像之前一样创建一个模板文件: - -````xml - - - - - - - {%{{{content}}}%} - - -```` - -* 布局模板必须具有 **{%{{{content}}}%}** 部分作为渲染的子内容的占位符. - -在模板定义提供程序中注册模板: - -````csharp -context.Add( - new TemplateDefinition( - "EmailLayout", - isLayout: true //SET isLayout! - ).WithVirtualFilePath( - "/Demos/EmailLayout/EmailLayout.tpl", - isInlineLocalized: true - ) -); -```` - -现在你可以将此模板用作任何其他模板的布局: - -````csharp -context.Add( - new TemplateDefinition( - name: "WelcomeEmail", - defaultCultureName: "en", - layout: "EmailLayout" //Set the LAYOUT - ).WithVirtualFilePath( - "/Demos/WelcomeEmail/Templates", - isInlineLocalized: false - ) -); -```` - -## 全局上下文 - -ABP传递 `model`,可用于访问模板内的模型. 如果需要,可以传递更多的全局变量. - -示例模板内容: - -```` -A global object value: {%{{{myGlobalObject}}}%} -```` - -模板假定它渲染上下文中的 `myGlobalObject` 对象. 你可以如下所示提供它: - -````csharp -var result = await _templateRenderer.RenderAsync( - "GlobalContextUsage", - globalContext: new Dictionary - { - {"myGlobalObject", "TEST VALUE"} - } -); -```` - -渲染的结果将是: - -```` -A global object value: TEST VALUE -```` - -## 高级功能 - -本节介绍文本模板系统的一些内部知识和高级用法. - -### 模板内容Provider - -`TemplateRenderer` 用于渲染模板,这是大多数情况下所需的模板. 但是你可以使用 `ITemplateContentProvider` 获取原始(未渲染的)模板内容. - -> `ITemplateRenderer` 内部使用 `ITemplateContentProvider` 获取原始模板内容. - -示例: - -````csharp -public class TemplateContentDemo : ITransientDependency -{ - private readonly ITemplateContentProvider _templateContentProvider; - - public TemplateContentDemo(ITemplateContentProvider templateContentProvider) - { - _templateContentProvider = templateContentProvider; - } - - public async Task RunAsync() - { - var result = await _templateContentProvider - .GetContentOrNullAsync("Hello"); - - Console.WriteLine(result); - } -} -```` - -结果是原始模板内容: - -```` -Hello {%{{{model.name}}}%} :) -```` - -* `GetContentOrNullAsync` 如果没有为请求的模板定义任何内容,则返回 `null`. -* 它可以获取 `cultureName` 参数,如果模板针对不同的文化具有不同的文件,则可以使用该参数(请参见上面的"多内容本地化"部分). - -### 模板内容贡献者 - -`ITemplateContentProvider` 服务使用 `ITemplateContentContributor` 实现来查找模板内容. 有一个预实现的内容贡献者 `VirtualFileTemplateContentContributor`,它从上面描述的虚拟文件系统中获取模板内容. - -你可以实现 `ITemplateContentContributor` 从另一个源读取原始模板内容. - -示例: - -````csharp -public class MyTemplateContentProvider - : ITemplateContentContributor, ITransientDependency -{ - public async Task GetOrNullAsync(TemplateContentContributorContext context) - { - var templateName = context.TemplateDefinition.Name; - - //TODO: Try to find content from another source - return null; - } -} +ABP框架提供了两个模板引擎: -```` +* **[Razor](Text-Templating-Razor.md)** +* **[Scriban](Text-Templating-Scriban.md)** -如果源无法找到内容, 则返回 `null`, `ITemplateContentProvider` 将回退到下一个贡献者. +你可以在同一个应用应用程序中使用不同的模板引擎, 或者创建一个新的自定义模板引擎. -### Template Definition Manager +## 源码 -`ITemplateDefinitionManager` 服务可用于获取模板定义(由模板定义提供程序创建). +查看开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). ## 另请参阅 * 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). * [本地化系统](Localization.md). -* [虚拟文件系统](Virtual-File-System.md). +* [虚拟文件系统](Virtual-File-System.md). \ No newline at end of file diff --git a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md index dd688f256c..338afe8967 100644 --- a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md +++ b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md @@ -48,7 +48,7 @@ namespace MyCompany.MyProject ```` -`abp-script-bundle`定义了一个带有**唯一名称**的样式包:`MyGlobalBundle`. 使用方法很容易理解. 让我们看看它是如何*工作的*: +`abp-style-bundle`定义了一个带有**唯一名称**的样式包:`MyGlobalBundle`. 使用方法很容易理解. 让我们看看它是如何*工作的*: * 当首次请求时,ABP从提供的文件中 **(延迟)lazy** 创建. 后续将从 **缓存** 中返回内容. 这意味着如果你有条件地将文件添加到包中,它只执行一次, 并且条件的任何更改都不会影响下一个请求的包. * 在`development`环境中ABP会将包文件**单独**添加到页面中, 其他环境(`staging`,`production`...)会自动捆绑和压缩. diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index f708415979..d66d11b3cf 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -409,14 +409,16 @@ "path": "Entities.md" }, { - "text": "值对象" + "text": "值对象", + "path": "Value-Objects.md" }, { "text": "仓储", "path": "Repositories.md" }, { - "text": "领域服务" + "text": "领域服务", + "path": "Domain-Services.md" }, { "text": "规约", @@ -808,7 +810,13 @@ "items": [ { "text": "CLI", - "path": "CLI.md" + "path": "CLI.md", + "items": [ + { + "text": "新命令示例", + "path": "CLI-New-Command-Samples.md" + } + ] }, { "text": "API文档", diff --git a/docs/zh-Hans/images/abp-book.png b/docs/zh-Hans/images/abp-book.png new file mode 100644 index 0000000000..7fd6c34c90 Binary files /dev/null and b/docs/zh-Hans/images/abp-book.png differ diff --git a/docs/zh-Hans/images/abp-select2-multiple.png b/docs/zh-Hans/images/abp-select2-multiple.png new file mode 100644 index 0000000000..bdc2d7cc95 Binary files /dev/null and b/docs/zh-Hans/images/abp-select2-multiple.png differ diff --git a/docs/zh-Hans/images/abp-select2-single.png b/docs/zh-Hans/images/abp-select2-single.png new file mode 100644 index 0000000000..219355dd66 Binary files /dev/null and b/docs/zh-Hans/images/abp-select2-single.png differ diff --git a/docs/zh-Hans/images/account-module-forgot-password.png b/docs/zh-Hans/images/account-module-forgot-password.png new file mode 100644 index 0000000000..92fce47a22 Binary files /dev/null and b/docs/zh-Hans/images/account-module-forgot-password.png differ diff --git a/docs/zh-Hans/images/account-module-login.png b/docs/zh-Hans/images/account-module-login.png new file mode 100644 index 0000000000..abde22db57 Binary files /dev/null and b/docs/zh-Hans/images/account-module-login.png differ diff --git a/docs/zh-Hans/images/account-module-manage-account.png b/docs/zh-Hans/images/account-module-manage-account.png new file mode 100644 index 0000000000..92a5f34d72 Binary files /dev/null and b/docs/zh-Hans/images/account-module-manage-account.png differ diff --git a/docs/zh-Hans/images/account-module-register.png b/docs/zh-Hans/images/account-module-register.png new file mode 100644 index 0000000000..f2b9c0587a Binary files /dev/null and b/docs/zh-Hans/images/account-module-register.png differ diff --git a/docs/zh-Hans/images/add-new-propert-to-user-database-extra-properties.png b/docs/zh-Hans/images/add-new-propert-to-user-database-extra-properties.png new file mode 100644 index 0000000000..90c697461e Binary files /dev/null and b/docs/zh-Hans/images/add-new-propert-to-user-database-extra-properties.png differ diff --git a/docs/zh-Hans/images/add-new-propert-to-user-database-field.png b/docs/zh-Hans/images/add-new-propert-to-user-database-field.png new file mode 100644 index 0000000000..334ba0aa35 Binary files /dev/null and b/docs/zh-Hans/images/add-new-propert-to-user-database-field.png differ diff --git a/docs/zh-Hans/images/add-new-property-enum.png b/docs/zh-Hans/images/add-new-property-enum.png new file mode 100644 index 0000000000..34a14b1bfb Binary files /dev/null and b/docs/zh-Hans/images/add-new-property-enum.png differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-form-validation-error-custom.png b/docs/zh-Hans/images/add-new-property-to-user-form-validation-error-custom.png new file mode 100644 index 0000000000..6bb1799e19 Binary files /dev/null and b/docs/zh-Hans/images/add-new-property-to-user-form-validation-error-custom.png differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-form-validation-error.png b/docs/zh-Hans/images/add-new-property-to-user-form-validation-error.png new file mode 100644 index 0000000000..dd2eb8ad1d Binary files /dev/null and b/docs/zh-Hans/images/add-new-property-to-user-form-validation-error.png differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-form.png b/docs/zh-Hans/images/add-new-property-to-user-form.png new file mode 100644 index 0000000000..bc12a6d5ad Binary files /dev/null and b/docs/zh-Hans/images/add-new-property-to-user-form.png differ diff --git a/docs/zh-Hans/images/add-new-property-to-user-table.png b/docs/zh-Hans/images/add-new-property-to-user-table.png new file mode 100644 index 0000000000..a2a15087cc Binary files /dev/null and b/docs/zh-Hans/images/add-new-property-to-user-table.png differ diff --git a/docs/zh-Hans/images/angular-module-dev-app-project.png b/docs/zh-Hans/images/angular-module-dev-app-project.png new file mode 100644 index 0000000000..08e0e9fd6c Binary files /dev/null and b/docs/zh-Hans/images/angular-module-dev-app-project.png differ diff --git a/docs/zh-Hans/images/angular-module-folder-structure.png b/docs/zh-Hans/images/angular-module-folder-structure.png new file mode 100644 index 0000000000..ae2333b295 Binary files /dev/null and b/docs/zh-Hans/images/angular-module-folder-structure.png differ diff --git a/docs/zh-Hans/images/blazor-generic-exception-message.png b/docs/zh-Hans/images/blazor-generic-exception-message.png new file mode 100644 index 0000000000..5e128003cf Binary files /dev/null and b/docs/zh-Hans/images/blazor-generic-exception-message.png differ diff --git a/docs/zh-Hans/images/blazor-user-friendly-exception.png b/docs/zh-Hans/images/blazor-user-friendly-exception.png new file mode 100644 index 0000000000..fa2a70d931 Binary files /dev/null and b/docs/zh-Hans/images/blazor-user-friendly-exception.png differ diff --git a/docs/zh-Hans/images/bookstore-visual-studio-solution-v3.png b/docs/zh-Hans/images/bookstore-visual-studio-solution-v3.png index 307e3516a5..f8ef7f2ba0 100644 Binary files a/docs/zh-Hans/images/bookstore-visual-studio-solution-v3.png and b/docs/zh-Hans/images/bookstore-visual-studio-solution-v3.png differ diff --git a/docs/zh-Hans/images/cmskit-module-blog-post-edit.png b/docs/zh-Hans/images/cmskit-module-blog-post-edit.png new file mode 100644 index 0000000000..528bc6b499 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-blog-post-edit.png differ diff --git a/docs/zh-Hans/images/cmskit-module-blog-posts-page.png b/docs/zh-Hans/images/cmskit-module-blog-posts-page.png new file mode 100644 index 0000000000..64d5d0e934 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-blog-posts-page.png differ diff --git a/docs/zh-Hans/images/cmskit-module-blogs-edit.png b/docs/zh-Hans/images/cmskit-module-blogs-edit.png new file mode 100644 index 0000000000..b2da4e5b5a Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-blogs-edit.png differ diff --git a/docs/zh-Hans/images/cmskit-module-blogs-feature-action.png b/docs/zh-Hans/images/cmskit-module-blogs-feature-action.png new file mode 100644 index 0000000000..c0f0b1d3f4 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-blogs-feature-action.png differ diff --git a/docs/zh-Hans/images/cmskit-module-blogs-page.png b/docs/zh-Hans/images/cmskit-module-blogs-page.png new file mode 100644 index 0000000000..6c150429b7 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-blogs-page.png differ diff --git a/docs/zh-Hans/images/cmskit-module-comment-page.png b/docs/zh-Hans/images/cmskit-module-comment-page.png new file mode 100644 index 0000000000..6647a6dd09 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-comment-page.png differ diff --git a/docs/zh-Hans/images/cmskit-module-comments-detail.png b/docs/zh-Hans/images/cmskit-module-comments-detail.png new file mode 100644 index 0000000000..686a70e1f8 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-comments-detail.png differ diff --git a/docs/zh-Hans/images/cmskit-module-features-dialog-2.png b/docs/zh-Hans/images/cmskit-module-features-dialog-2.png new file mode 100644 index 0000000000..b6dd409e0a Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-features-dialog-2.png differ diff --git a/docs/zh-Hans/images/cmskit-module-features-dialog.png b/docs/zh-Hans/images/cmskit-module-features-dialog.png new file mode 100644 index 0000000000..23aa2aa17a Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-features-dialog.png differ diff --git a/docs/zh-Hans/images/cmskit-module-features-scroll-index.png b/docs/zh-Hans/images/cmskit-module-features-scroll-index.png new file mode 100644 index 0000000000..0bc3dedcc5 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-features-scroll-index.png differ diff --git a/docs/zh-Hans/images/cmskit-module-global-resources-page.png b/docs/zh-Hans/images/cmskit-module-global-resources-page.png new file mode 100644 index 0000000000..9090c97412 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-global-resources-page.png differ diff --git a/docs/zh-Hans/images/cmskit-module-menus-page.png b/docs/zh-Hans/images/cmskit-module-menus-page.png new file mode 100644 index 0000000000..c265763788 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-menus-page.png differ diff --git a/docs/zh-Hans/images/cmskit-module-menus-public.png b/docs/zh-Hans/images/cmskit-module-menus-public.png new file mode 100644 index 0000000000..0b67064334 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-menus-public.png differ diff --git a/docs/zh-Hans/images/cmskit-module-pages-edit.png b/docs/zh-Hans/images/cmskit-module-pages-edit.png new file mode 100644 index 0000000000..cca074fdac Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-pages-edit.png differ diff --git a/docs/zh-Hans/images/cmskit-module-pages-page.png b/docs/zh-Hans/images/cmskit-module-pages-page.png new file mode 100644 index 0000000000..b64915493c Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-pages-page.png differ diff --git a/docs/zh-Hans/images/cmskit-module-ratings.png b/docs/zh-Hans/images/cmskit-module-ratings.png new file mode 100644 index 0000000000..f97c934f15 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-ratings.png differ diff --git a/docs/zh-Hans/images/cmskit-module-reactions.png b/docs/zh-Hans/images/cmskit-module-reactions.png new file mode 100644 index 0000000000..b915b85f91 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-reactions.png differ diff --git a/docs/zh-Hans/images/cmskit-module-tag-edit.png b/docs/zh-Hans/images/cmskit-module-tag-edit.png new file mode 100644 index 0000000000..eb1ebb3a52 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-tag-edit.png differ diff --git a/docs/zh-Hans/images/cmskit-module-tags-page.png b/docs/zh-Hans/images/cmskit-module-tags-page.png new file mode 100644 index 0000000000..f41b6ecbd0 Binary files /dev/null and b/docs/zh-Hans/images/cmskit-module-tags-page.png differ diff --git a/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render-solution.png b/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render-solution.png new file mode 100644 index 0000000000..77612b5d34 Binary files /dev/null and b/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render-solution.png differ diff --git a/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render.png b/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render.png new file mode 100644 index 0000000000..9d018e2ab9 Binary files /dev/null and b/docs/zh-Hans/images/data-table-colum-extension-blazor-component-render.png differ diff --git a/docs/zh-Hans/images/docs-create-project-v4.4.0.png b/docs/zh-Hans/images/docs-create-project-v4.4.0.png new file mode 100644 index 0000000000..fa30fcbc3a Binary files /dev/null and b/docs/zh-Hans/images/docs-create-project-v4.4.0.png differ diff --git a/docs/zh-Hans/images/docs-module_solution-explorer.png b/docs/zh-Hans/images/docs-module_solution-explorer.png index 2988ec4134..42b2c31143 100644 Binary files a/docs/zh-Hans/images/docs-module_solution-explorer.png and b/docs/zh-Hans/images/docs-module_solution-explorer.png differ diff --git a/docs/zh-Hans/images/extension-navigation-property-form.png b/docs/zh-Hans/images/extension-navigation-property-form.png new file mode 100644 index 0000000000..5e55ce3afd Binary files /dev/null and b/docs/zh-Hans/images/extension-navigation-property-form.png differ diff --git a/docs/zh-Hans/images/extension-navigation-property-table.png b/docs/zh-Hans/images/extension-navigation-property-table.png new file mode 100644 index 0000000000..a3f333919c Binary files /dev/null and b/docs/zh-Hans/images/extension-navigation-property-table.png differ diff --git a/docs/zh-Hans/images/features-modal.png b/docs/zh-Hans/images/features-modal.png index 0fce7ecf5c..74ee8aabc3 100644 Binary files a/docs/zh-Hans/images/features-modal.png and b/docs/zh-Hans/images/features-modal.png differ diff --git a/docs/zh-Hans/images/features-module-opening.png b/docs/zh-Hans/images/features-module-opening.png index a3899924ce..bd99604cd7 100644 Binary files a/docs/zh-Hans/images/features-module-opening.png and b/docs/zh-Hans/images/features-module-opening.png differ diff --git a/docs/zh-Hans/images/generated-static-client-proxies.png b/docs/zh-Hans/images/generated-static-client-proxies.png new file mode 100644 index 0000000000..4c1678aebd Binary files /dev/null and b/docs/zh-Hans/images/generated-static-client-proxies.png differ diff --git a/docs/zh-Hans/images/hello-template-razor.png b/docs/zh-Hans/images/hello-template-razor.png new file mode 100644 index 0000000000..1e0e39fcec Binary files /dev/null and b/docs/zh-Hans/images/hello-template-razor.png differ diff --git a/docs/zh-Hans/images/identity-module-menu.png b/docs/zh-Hans/images/identity-module-menu.png index ce2ee7e3e9..8083e2f722 100644 Binary files a/docs/zh-Hans/images/identity-module-menu.png and b/docs/zh-Hans/images/identity-module-menu.png differ diff --git a/docs/zh-Hans/images/identity-module-permissions.png b/docs/zh-Hans/images/identity-module-permissions.png index dfe0a9a022..24ef15e3b4 100644 Binary files a/docs/zh-Hans/images/identity-module-permissions.png and b/docs/zh-Hans/images/identity-module-permissions.png differ diff --git a/docs/zh-Hans/images/identity-module-roles.png b/docs/zh-Hans/images/identity-module-roles.png index 53df1cdc6d..c8e2c9cde1 100644 Binary files a/docs/zh-Hans/images/identity-module-roles.png and b/docs/zh-Hans/images/identity-module-roles.png differ diff --git a/docs/zh-Hans/images/identity-module-users.png b/docs/zh-Hans/images/identity-module-users.png index 33edb4cb78..7de6da77d4 100644 Binary files a/docs/zh-Hans/images/identity-module-users.png and b/docs/zh-Hans/images/identity-module-users.png differ diff --git a/docs/zh-Hans/images/implementing-domain-driven-design-book.png b/docs/zh-Hans/images/implementing-domain-driven-design-book.png new file mode 100644 index 0000000000..6568b46f66 Binary files /dev/null and b/docs/zh-Hans/images/implementing-domain-driven-design-book.png differ diff --git a/docs/zh-Hans/images/module-tenant-management-actions.png b/docs/zh-Hans/images/module-tenant-management-actions.png index 899e731f8c..d1f01c2e93 100644 Binary files a/docs/zh-Hans/images/module-tenant-management-actions.png and b/docs/zh-Hans/images/module-tenant-management-actions.png differ diff --git a/docs/zh-Hans/images/module-tenant-management-new-tenant.png b/docs/zh-Hans/images/module-tenant-management-new-tenant.png index 7f0f50b42a..ab9ee6ed56 100644 Binary files a/docs/zh-Hans/images/module-tenant-management-new-tenant.png and b/docs/zh-Hans/images/module-tenant-management-new-tenant.png differ diff --git a/docs/zh-Hans/images/module-tenant-management-page.png b/docs/zh-Hans/images/module-tenant-management-page.png index 5ed6cc63bd..563c64aac8 100644 Binary files a/docs/zh-Hans/images/module-tenant-management-page.png and b/docs/zh-Hans/images/module-tenant-management-page.png differ diff --git a/docs/zh-Hans/images/multiple-file-template-razor.png b/docs/zh-Hans/images/multiple-file-template-razor.png new file mode 100644 index 0000000000..85f87e84f4 Binary files /dev/null and b/docs/zh-Hans/images/multiple-file-template-razor.png differ diff --git a/docs/zh-Hans/images/optimistic-concurrency.png b/docs/zh-Hans/images/optimistic-concurrency.png new file mode 100644 index 0000000000..85253594c8 Binary files /dev/null and b/docs/zh-Hans/images/optimistic-concurrency.png differ diff --git a/docs/zh-Hans/images/page-header-toolbar-blazor.png b/docs/zh-Hans/images/page-header-toolbar-blazor.png new file mode 100644 index 0000000000..a4d0454893 Binary files /dev/null and b/docs/zh-Hans/images/page-header-toolbar-blazor.png differ diff --git a/docs/zh-Hans/images/page-toolbar-button-blazor.png b/docs/zh-Hans/images/page-toolbar-button-blazor.png new file mode 100644 index 0000000000..fb907e797a Binary files /dev/null and b/docs/zh-Hans/images/page-toolbar-button-blazor.png differ diff --git a/docs/zh-Hans/images/page-toolbar-button.png b/docs/zh-Hans/images/page-toolbar-button.png new file mode 100644 index 0000000000..05e3f9da11 Binary files /dev/null and b/docs/zh-Hans/images/page-toolbar-button.png differ diff --git a/docs/zh-Hans/images/page-toolbar-custom-component-blazor.png b/docs/zh-Hans/images/page-toolbar-custom-component-blazor.png new file mode 100644 index 0000000000..fb0856fa03 Binary files /dev/null and b/docs/zh-Hans/images/page-toolbar-custom-component-blazor.png differ diff --git a/docs/zh-Hans/images/page-toolbar-custom-component.png b/docs/zh-Hans/images/page-toolbar-custom-component.png new file mode 100644 index 0000000000..a15ef4854c Binary files /dev/null and b/docs/zh-Hans/images/page-toolbar-custom-component.png differ diff --git a/docs/zh-Hans/images/permissions-module-dialog.png b/docs/zh-Hans/images/permissions-module-dialog.png index 5af4cbef43..6b190484a2 100644 Binary files a/docs/zh-Hans/images/permissions-module-dialog.png and b/docs/zh-Hans/images/permissions-module-dialog.png differ diff --git a/docs/zh-Hans/images/permissions-module-open-dialog.png b/docs/zh-Hans/images/permissions-module-open-dialog.png index 06821d8c53..46631fe126 100644 Binary files a/docs/zh-Hans/images/permissions-module-open-dialog.png and b/docs/zh-Hans/images/permissions-module-open-dialog.png differ diff --git a/docs/zh-Hans/images/replace-email-layout-razor.png b/docs/zh-Hans/images/replace-email-layout-razor.png new file mode 100644 index 0000000000..84ff6c1080 Binary files /dev/null and b/docs/zh-Hans/images/replace-email-layout-razor.png differ diff --git a/docs/zh-Hans/images/static-js-proxy-example.png b/docs/zh-Hans/images/static-js-proxy-example.png new file mode 100644 index 0000000000..575957b9ae Binary files /dev/null and b/docs/zh-Hans/images/static-js-proxy-example.png differ diff --git a/docs/zh-Hans/images/table-column-extension-example-blazor.png b/docs/zh-Hans/images/table-column-extension-example-blazor.png new file mode 100644 index 0000000000..3079fa278c Binary files /dev/null and b/docs/zh-Hans/images/table-column-extension-example-blazor.png differ diff --git a/docs/zh-Hans/images/table-column-extension-example.png b/docs/zh-Hans/images/table-column-extension-example.png new file mode 100644 index 0000000000..84d87bf46c Binary files /dev/null and b/docs/zh-Hans/images/table-column-extension-example.png differ diff --git a/docs/zh-Hans/images/upgrade-diff-empty-folders.png b/docs/zh-Hans/images/upgrade-diff-empty-folders.png new file mode 100644 index 0000000000..9f0256061a Binary files /dev/null and b/docs/zh-Hans/images/upgrade-diff-empty-folders.png differ diff --git a/docs/zh-Hans/images/user-action-blazor-extension-click-me.png b/docs/zh-Hans/images/user-action-blazor-extension-click-me.png new file mode 100644 index 0000000000..fb1ea80182 Binary files /dev/null and b/docs/zh-Hans/images/user-action-blazor-extension-click-me.png differ diff --git a/docs/zh-Hans/images/user-action-extension-click-me.png b/docs/zh-Hans/images/user-action-extension-click-me.png new file mode 100644 index 0000000000..4046630f48 Binary files /dev/null and b/docs/zh-Hans/images/user-action-extension-click-me.png differ diff --git a/docs/zh-Hans/images/user-action-extension-on-blazor-project.png b/docs/zh-Hans/images/user-action-extension-on-blazor-project.png new file mode 100644 index 0000000000..d11ea328b6 Binary files /dev/null and b/docs/zh-Hans/images/user-action-extension-on-blazor-project.png differ diff --git a/docs/zh-Hans/images/user-action-extension-on-solution.png b/docs/zh-Hans/images/user-action-extension-on-solution.png new file mode 100644 index 0000000000..5f58d3f302 Binary files /dev/null and b/docs/zh-Hans/images/user-action-extension-on-solution.png differ diff --git a/docs/zh-Hans/images/winmerge-comparison-result.png b/docs/zh-Hans/images/winmerge-comparison-result.png new file mode 100644 index 0000000000..dbce7cb52e Binary files /dev/null and b/docs/zh-Hans/images/winmerge-comparison-result.png differ diff --git a/docs/zh-Hans/images/winmerge-file-diff.png b/docs/zh-Hans/images/winmerge-file-diff.png new file mode 100644 index 0000000000..645d3de696 Binary files /dev/null and b/docs/zh-Hans/images/winmerge-file-diff.png differ diff --git a/docs/zh-Hans/images/winmerge-open-folders.png b/docs/zh-Hans/images/winmerge-open-folders.png new file mode 100644 index 0000000000..53327601d4 Binary files /dev/null and b/docs/zh-Hans/images/winmerge-open-folders.png differ diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs index 92bfc9ddb3..749f1fc935 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Form/AbpInputTagHelperService.cs @@ -113,7 +113,7 @@ public class AbpInputTagHelperService : AbpTagHelperService protected virtual TagHelper GetInputTagHelper(TagHelperContext context, TagHelperOutput output) { - if (TagHelper.AspFor.ModelExplorer.GetAttribute"; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + // Support: IE <=9 only + // IE <=9 replaces "; + support.option = !!div.lastChild; +} )(); + + +// We have to close these tags to support XHTML (#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 or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _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, "" ]; +} + + +function getAll( context, tag ) { + + // Support: IE <=9 - 11 only + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( toType( elem ) === "object" ) { + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android <=4.0 only, PhantomJS 1 only + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( attached ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + +// Support: IE <=9 only +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // 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 instance trees (trac-13180) + cur.nodeType && + + // Support: Firefox <=42 + // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) + // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click + // Support: IE 11 only + // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + click: { + + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); + } + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + // Return non-false to allow normal event-path propagation + return true; + }, + + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + return result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android <=2.3 only + src.returnValue === false ? + returnTrue : + returnFalse; + + // Create target properties + // Support: Safari <=6 - 7 only + // Target should not be a text node (#504, #13143) + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || Date.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Includes all common event props including KeyEvent and MouseEvent specific props +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + + which: function( event ) { + var button = event.button; + + // Add which for key events + if ( event.which == null && rkeyEvent.test( event.type ) ) { + return event.charCode != null ? event.charCode : event.keyCode; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { + if ( button & 1 ) { + return 1; + } + + if ( button & 2 ) { + return 3; + } + + if ( button & 4 ) { + return 2; + } + + return 0; + } + + return event.which; + } +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + delegateType: delegateType + }; +} ); + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + + // 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 = /\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 (#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 { + 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 Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <=35 - 45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: Android <=4.0 only, PhantomJS 1 only + // .get() because push.apply(_, arraylike) throws on ancient WebKit + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var getStyles = function( elem ) { + + // Support: IE <=11 only, Firefox <=30 (#15098, #14150) + // IE throws on elements created in popups + // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var 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 (#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! + 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"; + tr.style.height = "1px"; + trChild.style.height = "9px"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + + // Support: Firefox 51+ + // Retrieving style before computed somehow + // fixes an issue with getting wrong values + // on detached elements + style = elem.style; + + computed = computed || getStyles( elem ); + + // getPropertyValue is needed for: + // .css('filter') (IE 9 only, #12537) + // .css('--customProperty) (#3144) + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Android Browser returns percentage for some values, + // but width seems to be reliably pixels. + // This is against the CSSOM draft spec: + // https://drafts.csswg.org/cssom/#resolved-values + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + // Support: IE <=9 - 11 only + // IE returns zIndex value as an integer. + ret + "" : + ret; +} + + +function addGetHookIf( conditionFn, hookFn ) { + + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + + // Hook not needed (or it's not possible to use it due + // to missing dependency), remove it. + delete this.get; + return; + } + + // Hook needed; redefine it so that the support test is not executed again. + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +// Return a vendor-prefixed property or undefined +function vendorPropName( name ) { + + // Check for vendor prefixed names + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + // Any relative (+/-) values have already been + // normalized at this point + var matches = rcssNum.exec( value ); + return matches ? + + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; + + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { + + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" + } else { + + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + // Start with computed style + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + + // 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, + "columnCount": true, + "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, + "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: {}, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + // Make sure that we're working with the right name. We don't + // want to query the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Gets hook for the prefixed version, then unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // Convert "+=" or "-=" to relative numbers (#7345) + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + // Fixes bug #9237 + type = "number"; + } + + // Make sure that null and NaN values aren't set (#7116) + if ( value == null || value !== value ) { + return; + } + + // If a number was passed in, add the unit (except for certain CSS properties) + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + // background-* props affect original clone's values + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + // Make sure that we're working with the right name. We don't + // want to modify the value if it is a CSS custom property + // since they are user-defined. + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + // Try prefixed name followed by the unprefixed name + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + // Convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Make numeric if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + // Certain elements can have dimension info if we invisibly show them + // but it must have a current display style that would benefit + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + // Support: Safari 8+ + // Table columns in Safari have non-zero offsetWidth & zero + // getBoundingClientRect().width unless display is changed. + // Support: IE <=11 only + // Running getBoundingClientRect on a disconnected node + // in IE throws an error. + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + // Convert to pixels if value adjustment is needed + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +// These hooks are used by animate to expand properties +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + // Assumes a single number if not a string + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + // Use a property on the element directly when it is not a DOM element, + // or when there is no matching style property that exists. + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + // Passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails. + // Simple values such as "10px" are parsed to Float; + // complex values such as "rotate(1rad)" are returned as-is. + result = jQuery.css( tween.elem, tween.prop, "" ); + + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + // Use step hook for back compat. + // Use cssHook if its there. + // Use .style if available and use plain properties where available. + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +// Support: IE <=9 only +// Panic based approach to setting things on disconnected nodes +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +// Back compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +// Animations created synchronously will run synchronously +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // If we include width, step value is 1 to do all cssExpand values, + // otherwise step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + // We're done with this property + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + // Queue-skipping animations hijack the fx hooks + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + // Ensure the complete handler is called before this completes + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + // Detect show/hide animations + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // Pretend to be hidden if this is a "show" and + // there is still data from a stopped show/hide + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + // Ignore all other no-op show/hide data + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + // Bail out if this is a no-op like .hide().hide() + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + // Restrict "overflow" and "display" styles during box animations + if ( isBox && elem.nodeType === 1 ) { + + // Support: IE <=9 - 11, Edge 12 - 15 + // Record all 3 overflow attributes because IE does not infer the shorthand + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Identify a display type, preferring old show/hide data over the CSS cascade + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + // Get nonempty value(s) by temporarily forcing visibility + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + // Animate inline elements as inline-block + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + // Restore the original display value at the end of pure show/hide animations + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + // Implement show/hide animations + propTween = false; + for ( prop in orig ) { + + // General show/hide setup for this element animation + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + // Store hidden/visible for toggle so `.stop().toggle()` "reverses" + if ( toggle ) { + dataShow.hidden = !hidden; + } + + // Show elements before animating them + if ( hidden ) { + showHide( [ elem ], true ); + } + + /* eslint-disable no-loop-func */ + + anim.done( function() { + + /* eslint-enable no-loop-func */ + + // The final step of a "hide" animation is actually hiding the element + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + // Per-property setup + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + // Not quite $.extend, this won't overwrite existing keys. + // Reusing 'index' because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + // Don't match elem in the :animated selector + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + // Support: Android 2.3 only + // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + // If there's more to do, yield + if ( percent < 1 && length ) { + return remaining; + } + + // If this was an empty animation, synthesize a final progress notification + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + // Resolve the animation and report its conclusion + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + // If we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + // Resolve when we played the last frame; otherwise, reject + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + // Attach callbacks from options + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + // Go to the end state if fx are off + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + // Normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + // Show any hidden elements after setting opacity to 0 + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + // Animate to the value specified + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // Start the next in the queue if the last step wasn't forced. + // Timers currently will call their complete callbacks, which + // will dequeue but only if they were gotoEnd. + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // Enable finishing flag on private data + data.finish = true; + + // Empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // Look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // Look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // Turn off finishing flag + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +// Generate shortcuts for custom animations +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + // Run the timer and safely remove it when done (allowing for external removal) + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: Android <=4.3 only + // Default value for a checkbox should be "on" + support.checkOn = input.value !== ""; + + // Support: IE <=11 only + // Must access selectedIndex to make default options select + support.optSelected = opt.selected; + + // Support: IE <=11 only + // An input loses its value after becoming a radio + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set attributes on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + // Attribute hooks are determined by the lowercase version + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + // Attribute names can contain non-HTML whitespace characters + // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + // Don't get/set properties on text, comment and attribute nodes + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + // Support: IE <=9 - 11 only + // elem.tabIndex doesn't always return the + // correct value when it hasn't been explicitly set + // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +// Support: IE <=11 only +// Accessing the selectedIndex property +// forces the browser to respect setting selected +// on the option +// The getter ensures a default option is selected +// when in an optgroup +// eslint rule "no-unused-expressions" is disabled for this code +// since it considers such accessions noop +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + /* eslint no-unused-expressions: "off" */ + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + + + + // Strip and collapse whitespace according to HTML spec + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, curValue, clazz, j, finalValue, + i = 0; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classes = classesToArray( value ); + + if ( classes.length ) { + while ( ( elem = this[ i++ ] ) ) { + curValue = getClass( elem ); + + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + j = 0; + while ( ( clazz = classes[ j++ ] ) ) { + + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) > -1 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // Only assign if different to avoid unneeded rendering. + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + elem.setAttribute( "class", finalValue ); + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + return this.each( function() { + var className, i, self, classNames; + + if ( isValidValue ) { + + // Toggle individual class names + i = 0; + self = jQuery( this ); + classNames = classesToArray( value ); + + while ( ( className = classNames[ i++ ] ) ) { + + // Check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + // Store className if set + dataPriv.set( this, "__className__", className ); + } + + // If the element has a class name or if we're passed `false`, + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + + + + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + // Handle most common string cases + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + // Handle cases where value is null/undef or number + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + // Support: IE <=10 - 11 only + // option.text throws exceptions (#14686, #14858) + // Strip and collapse whitespace + // https://html.spec.whatwg.org/#strip-and-collapse-whitespace + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Support: IE <=9 only + // IE8-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + + // Don't return options that are disabled or in a disabled optgroup + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + /* eslint-disable no-cond-assign */ + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + /* eslint-enable no-cond-assign */ + } + + // Force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +// Radios and checkboxes getter/setter +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + + + + +// Return jQuery for attributes-only inclusion + + +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( + dataPriv.get( cur, "events" ) || 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 (#6170) + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + // Piggyback on a donor event to simulate a different one + // Used only for `focus(in | out)` events + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + + +// Support: Firefox <=44 +// Firefox doesn't have focus(in | out) events +// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +// +// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 +// focus(in | out) events fire after focus & blur events, +// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order +// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 +if ( !support.focusin ) { + jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + + // Handle: regular nodes (via `this.ownerDocument`), window + // (via `this.document`) & document (via `this`). + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + attaches = dataPriv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + dataPriv.remove( doc, fix ); + + } else { + dataPriv.access( doc, fix, attaches ); + } + } + }; + } ); +} +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE 9 - 11 only + // IE throws on parseFromString with invalid input. + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) { + xml = undefined; + } + + if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; +}; + + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + + // Item is non-scalar (array or object), encode its numeric index. + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + // Serialize scalar item. + add( prefix, obj ); + } +} + +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + // If value is a function, invoke it and use its return value + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + // If an array was passed in, assume that it is an array of form elements. + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + // Return the resulting serialization + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ) + .filter( function() { + var type = this.type; + + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ) + .map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + // #7653, #8125, #8152: local protocol detection + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\//, + + /* Prefilters + * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) + * 2) These are called: + * - BEFORE asking for a transport + * - AFTER param serialization (s.data is a string if s.processData is true) + * 3) key is the dataType + * 4) the catchall symbol "*" can be used + * 5) execution will start with transport dataType and THEN continue down to "*" if needed + */ + prefilters = {}, + + /* Transports bindings + * 1) key is the dataType + * 2) the catchall symbol "*" can be used + * 3) selection will start with transport dataType and THEN go to "*" if needed + */ + transports = {}, + + // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression + allTypes = "*/".concat( "*" ), + + // Anchor tag for parsing the document origin + originAnchor = document.createElement( "a" ); + originAnchor.href = location.href; + +// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport +function addToPrefiltersOrTransports( structure ) { + + // dataTypeExpression is optional and defaults to "*" + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + // For each dataType in the dataTypeExpression + while ( ( dataType = dataTypes[ i++ ] ) ) { + + // Prepend if requested + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + // Otherwise append + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +// Base inspection function for prefilters and transports +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +// A special extend for ajax options +// that takes "flat" options (not to be deep extended) +// Fixes #9887 +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + // Or just use first one + finalDataType = finalDataType || firstDataType; + } + + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); + + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + // Convert to each sequential dataType + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { + + current = prev; + + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + // Counter for holding the number of active queries + active: 0, + + // Last-Modified header cache for next request + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + /* + timeout: 0, + data: null, + dataType: null, + username: null, + password: null, + cache: null, + throws: false, + traditional: false, + headers: {}, + */ + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + // Data converters + // Keys separate source (or catchall "*") and destination types with a single space + converters: { + + // Convert anything to text + "* text": String, + + // Text to html (true = no transformation) + "text html": true, + + // Evaluate text as a json expression + "text json": JSON.parse, + + // Parse text as xml + "text xml": jQuery.parseXML + }, + + // For options that shouldn't be deep extended: + // you can add your own custom options here if + // and when you create one that shouldn't be + // deep extended (see ajaxExtend) + flatOptions: { + url: true, + context: true + } + }, + + // Creates a full fledged settings object into target + // with both ajaxSettings and settings fields. + // If target is omitted, writes into ajaxSettings. + ajaxSetup: function( target, settings ) { + return settings ? + + // Building a settings object + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + // Extending ajaxSettings + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + // Main method + ajax: function( url, options ) { + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + var transport, + + // URL without anti-cache param + cacheURL, + + // Response headers + responseHeadersString, + responseHeaders, + + // timeout handle + timeoutTimer, + + // Url cleanup var + urlAnchor, + + // Request state (becomes false upon send and true upon completion) + completed, + + // To know if global events are to be dispatched + fireGlobals, + + // Loop variable + i, + + // uncached part of the url + uncached, + + // Create the final options object + s = jQuery.ajaxSetup( {}, options ), + + // Callbacks context + callbackContext = s.context || s, + + // Context for global events is callbackContext if it is a DOM node or jQuery collection + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + // Deferreds + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + // Status-dependent callbacks + statusCode = s.statusCode || {}, + + // Headers (they are sent all at once) + requestHeaders = {}, + requestHeadersNames = {}, + + // Default abort message + strAbort = "canceled", + + // Fake xhr + jqXHR = { + readyState: 0, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + // Raw string + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + // Caches the header + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + // Overrides response content-type header + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + // Status-dependent callbacks + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + // Execute the appropriate callbacks + jqXHR.always( map[ jqXHR.status ] ); + } else { + + // Lazy-add the new callbacks in a way that preserves old ones + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + // Cancel the request + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + // Attach deferreds + deferred.promise( jqXHR ); + + // Add protocol if not provided (prefilters might expect it) + // Handle falsy url in the settings object (#10093: consistency with old signature) + // We also use the url parameter if available + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + // Alias method option to type as per ticket #12004 + s.type = options.method || options.type || s.method || s.type; + + // Extract dataTypes list + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + // A cross-domain request is in order when the origin doesn't match the current origin. + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + // Support: IE <=8 - 11, Edge 12 - 15 + // IE throws exception on accessing the href property if url is malformed, + // e.g. http://example.com:80x/ + try { + urlAnchor.href = s.url; + + // Support: IE <=8 - 11 only + // Anchor's host property isn't correctly set when s.url is relative + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + // If there is an error parsing the URL, assume it is crossDomain, + // it can be rejected by the transport if it is invalid + s.crossDomain = true; + } + } + + // Convert data if not already a string + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + // Apply prefilters + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + // If request was aborted inside a prefilter, stop there + if ( completed ) { + return jqXHR; + } + + // We can fire global events as of now if asked to + // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) + fireGlobals = jQuery.event && s.global; + + // Watch for a new set of requests + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + // Uppercase the type + s.type = s.type.toUpperCase(); + + // Determine if request has content + s.hasContent = !rnoContent.test( s.type ); + + // Save the URL in case we're toying with the If-Modified-Since + // and/or If-None-Match header later on + // Remove hash to simplify url manipulation + cacheURL = s.url.replace( rhash, "" ); + + // More options handling for requests with no content + if ( !s.hasContent ) { + + // Remember the hash so we can put it back + uncached = s.url.slice( cacheURL.length ); + + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + // #9682: remove data so that it's not used in an eventual retry + delete s.data; + } + + // Add or update anti-cache param if needed + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + // Put hash and anti-cache on the URL that will be requested (gh-1732) + s.url = cacheURL + uncached; + + // Change '%20' to '+' if this is encoded form body content (gh-2658) + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + // Set the correct header, if data is being sent + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + // Set the Accepts header for the server, depending on the dataType + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + // Check for headers option + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + // Abort if not done already and return + return jqXHR.abort(); + } + + // Aborting is no longer a cancellation + strAbort = "abort"; + + // Install callbacks on deferreds + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + // Get transport + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + // If no transport, we auto-abort + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + // Send global event + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + // If request was aborted inside ajaxSend, stop there + if ( completed ) { + return jqXHR; + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + // Rethrow post-completion exceptions + if ( completed ) { + throw e; + } + + // Propagate others as results + done( -1, e ); + } + } + + // Callback for when everything is done + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + // Ignore repeat invocations + if ( completed ) { + return; + } + + completed = true; + + // Clear timeout if it exists + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + // Dereference transport for early garbage collection + // (no matter how long the jqXHR object will be used) + transport = undefined; + + // Cache response headers + responseHeadersString = headers || ""; + + // Set readyState + jqXHR.readyState = status > 0 ? 4 : 0; + + // Determine if successful + isSuccess = status >= 200 && status < 300 || status === 304; + + // Get response data + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + // Use a noop converter for missing script + if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + s.converters[ "text script" ] = function() {}; + } + + // Convert no matter what (that way responseXXX fields are always set) + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + // If successful, handle type chaining + if ( isSuccess ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + // if no content + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + // if not modified + } else if ( status === 304 ) { + statusText = "notmodified"; + + // If we have data, let's convert it + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + // Extract error from statusText and normalize for non-aborts + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + // Set data for the fake xhr object + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + // Success/Error + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + // Status-dependent callbacks + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + // Complete + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + // Handle the global AJAX counter + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + // Shift arguments if data argument was omitted + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + // The url can be an options object (which then must have .url) + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + // Make this explicit, since user can override this through ajaxSetup (#11264) + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + + + + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + // File protocol always yields status code 0, assume 200 + 0: 200, + + // Support: IE <=9 only + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + // Support: IE <=9 only + // On a manual native abort, IE9 throws + // errors on any property access that is not readyState + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + // File: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + // Support: IE <=9 only + // IE9 has no XHR2 but throws on binary (trac-11426) + // For XHR2 non-text, let the caller handle it (gh-2498) + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; + }; + + // Listen to events + xhr.onload = callback(); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + + // Support: IE 9 only + // Use onreadystatechange to replace onabort + // to handle uncaught aborts + if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; + } else { + xhr.onreadystatechange = function() { + + // Check readyState before timeout as it changes + if ( xhr.readyState === 4 ) { + + // Allow onerror to be called first, + // but that will not handle a native abort + // Also, save errorCallback to a variable + // as xhr.onerror cannot be accessed + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; + } + + // Create the abort callback + callback = callback( "abort" ); + + try { + + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; + } + } + }, + + abort: function() { + if ( callback ) { + callback(); + } + } + }; + } +} ); + + + + +// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) +jQuery.ajaxPrefilter( function( s ) { + if ( s.crossDomain ) { + s.contents.script = false; + } +} ); + +// Install script dataType +jQuery.ajaxSetup( { + accepts: { + script: "text/javascript, application/javascript, " + + "application/ecmascript, application/x-ecmascript" + }, + contents: { + script: /\b(?:java|ecma)script\b/ + }, + converters: { + "text script": function( text ) { + jQuery.globalEval( text ); + return text; + } + } +} ); + +// Handle cache's special case and crossDomain +jQuery.ajaxPrefilter( "script", function( s ) { + if ( s.cache === undefined ) { + s.cache = false; + } + if ( s.crossDomain ) { + s.type = "GET"; + } +} ); + +// Bind script tag hack transport +jQuery.ajaxTransport( "script", function( s ) { + + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { + var script, callback; + return { + send: function( _, complete ) { + script = jQuery( "