├── src
├── web
│ └── assets
│ │ └── dist
│ │ ├── assets
│ │ ├── app--MQxI0fB.js
│ │ ├── app-BgTVgbp1.css
│ │ ├── app--MQxI0fB.js.map
│ │ ├── welcome-BP_XNmLM.js.gz
│ │ ├── welcome-BP_XNmLM.js.map.gz
│ │ └── welcome-BP_XNmLM.js
│ │ ├── manifest.json
│ │ └── img
│ │ └── InstantAnalytics-icon.svg
├── templates
│ ├── _includes
│ │ ├── macros.twig
│ │ └── gtag.twig
│ ├── _layouts
│ │ └── instantanalytics-cp.twig
│ ├── welcome.twig
│ └── settings.twig
├── assetbundles
│ └── instantanalytics
│ │ ├── InstantAnalyticsAsset.php
│ │ └── InstantAnalyticsWelcomeAsset.php
├── ga4
│ ├── Service.php
│ ├── events
│ │ └── PageViewEvent.php
│ ├── ComponentFactory.php
│ └── Analytics.php
├── controllers
│ └── TrackController.php
├── icon.svg
├── translations
│ └── en
│ │ └── instant-analytics.php
├── services
│ ├── ServicesTrait.php
│ ├── Ga4.php
│ └── Commerce.php
├── variables
│ └── InstantAnalyticsVariable.php
├── config.php
├── twigextensions
│ └── InstantAnalyticsTwigExtension.php
├── models
│ └── Settings.php
├── helpers
│ ├── Field.php
│ └── Analytics.php
└── InstantAnalytics.php
├── phpstan.neon
├── CHANGELOG.md
├── ecs.php
├── Makefile
├── composer.json
├── LICENSE.md
└── README.md
/src/web/assets/dist/assets/app--MQxI0fB.js:
--------------------------------------------------------------------------------
1 |
2 | //# sourceMappingURL=app--MQxI0fB.js.map
3 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/app-BgTVgbp1.css:
--------------------------------------------------------------------------------
1 | .block{display:block}.inline-block{display:inline-block}
2 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/app--MQxI0fB.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"app--MQxI0fB.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon
3 |
4 | parameters:
5 | level: 5
6 | paths:
7 | - src
8 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/welcome-BP_XNmLM.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-instantanalytics-ga4/develop-v5/src/web/assets/dist/assets/welcome-BP_XNmLM.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/welcome-BP_XNmLM.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-instantanalytics-ga4/develop-v5/src/web/assets/dist/assets/welcome-BP_XNmLM.js.map.gz
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Instant Analytics GA4 Changelog
2 |
3 | ## 5.0.1 - 2024.09.14
4 | ### Fixed
5 | * Fixed an inadvertant dependency on SEOmatic ([#35](https://github.com/nystudio107/craft-instantanalytics-ga4/issues/35))
6 |
7 | ## 5.0.0 - 2024.07.09
8 | ### Added
9 | * Initial Craft 5 release
10 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | paths([
8 | __DIR__ . '/src',
9 | __FILE__,
10 | ]);
11 | $ecsConfig->parallel();
12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]);
13 | };
14 |
--------------------------------------------------------------------------------
/src/templates/_includes/macros.twig:
--------------------------------------------------------------------------------
1 | {% macro configWarning(setting, file) -%}
2 | {%- set configArray = craft.app.config.getConfigFromFile(file) -%}
3 | {%- if configArray[setting] is defined -%}
4 | {{- "This is being overridden by the `#{setting}` setting in the `config/#{file}.php` file." |raw }}
5 | {%- else -%}
6 | {{ false }}
7 | {%- endif -%}
8 | {%- endmacro %}
9 |
--------------------------------------------------------------------------------
/src/templates/_includes/gtag.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
--------------------------------------------------------------------------------
/src/templates/_layouts/instantanalytics-cp.twig:
--------------------------------------------------------------------------------
1 | {% extends "_layouts/cp" %}
2 |
3 | {% block head %}
4 | {{ parent() }}
5 | {% set tagOptions = {
6 | 'depends': [
7 | 'nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsAsset'
8 | ],
9 | } %}
10 | {{ craft.instantAnalytics.register('src/js/app.ts', false, tagOptions, tagOptions) }}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/src/web/assets/dist/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "src/js/app.ts": {
3 | "file": "assets/app--MQxI0fB.js",
4 | "name": "app",
5 | "src": "src/js/app.ts",
6 | "isEntry": true,
7 | "css": [
8 | "assets/app-BgTVgbp1.css"
9 | ]
10 | },
11 | "src/js/welcome.ts": {
12 | "file": "assets/welcome-BP_XNmLM.js",
13 | "name": "welcome",
14 | "src": "src/js/welcome.ts",
15 | "isEntry": true
16 | }
17 | }
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | MAJOR_VERSION?=5
2 | PLUGINDEV_PROJECT_DIR?=/Users/andrew/webdev/sites/plugindev/cms_v${MAJOR_VERSION}/
3 | VENDOR?=nystudio107
4 | PROJECT_PATH?=${VENDOR}/$(shell basename $(CURDIR))
5 |
6 | .PHONY: dev docs release
7 |
8 | # Start up the buildchain dev server
9 | dev:
10 | ${MAKE} -C buildchain/ dev
11 | # Start up the docs dev server
12 | docs:
13 | ${MAKE} -C docs/ dev
14 | # Run code quality tools, tests, and build the buildchain & docs in preparation for a release
15 | release: --code-quality --code-tests --buildchain-clean-build --docs-clean-build
16 | # The internal targets used by the dev & release targets
17 | --buildchain-clean-build:
18 | ${MAKE} -C buildchain/ clean
19 | ${MAKE} -C buildchain/ image-build
20 | ${MAKE} -C buildchain/ build
21 | --code-quality:
22 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- ecs check vendor/${PROJECT_PATH}/src --fix
23 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- phpstan analyze -c vendor/${PROJECT_PATH}/phpstan.neon
24 | --code-tests:
25 | --docs-clean-build:
26 | ${MAKE} -C docs/ clean
27 | ${MAKE} -C docs/ image-build
28 | ${MAKE} -C docs/ fix
29 |
--------------------------------------------------------------------------------
/src/assetbundles/instantanalytics/InstantAnalyticsAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = '@nystudio107/instantanalyticsGa4/web/assets/dist';
29 |
30 | // define the dependencies
31 | $this->depends = [
32 | CpAsset::class,
33 | VueAsset::class,
34 | ];
35 |
36 | parent::init();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/assetbundles/instantanalytics/InstantAnalyticsWelcomeAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = '@nystudio107/instantanalyticsGa4/web/assets/dist';
33 |
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | InstantAnalyticsAsset::class,
38 | ];
39 |
40 | parent::init();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/ga4/Service.php:
--------------------------------------------------------------------------------
1 | additionalParams[$name]);
29 | } else {
30 | $this->additionalParams[$name] = $value;
31 | }
32 | }
33 |
34 | public function deleteAdditionalQueryParam(string $name): void
35 | {
36 | unset($this->additionalParams[$name]);
37 | }
38 |
39 | public function getQueryParameters(): array
40 | {
41 | $parameters = parent::getQueryParameters();
42 |
43 | // Return without overwriting existing
44 | return $parameters + $this->additionalParams;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/ga4/events/PageViewEvent.php:
--------------------------------------------------------------------------------
1 | eventName, $paramList);
34 | }
35 |
36 | public function validate()
37 | {
38 | parent::validate();
39 |
40 | if (empty($this->getPageTitle())) {
41 | throw new ValidationException('Field "page_title" is required.', ErrorCode::VALIDATION_FIELD_REQUIRED, 'page_title');
42 | }
43 |
44 | if (empty($this->getPageLocation())) {
45 | throw new ValidationException('Field "page_location" is required if "value" is set', ErrorCode::VALIDATION_FIELD_REQUIRED, 'page_location');
46 | }
47 |
48 | return true;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/controllers/TrackController.php:
--------------------------------------------------------------------------------
1 | ga4->addPageViewEvent($url, $title);
43 | $this->redirect($url, 200);
44 | }
45 |
46 | /**
47 | * @param string $url
48 | * @param string $eventName
49 | * @param array $params
50 | */
51 | public function actionTrackEventUrl(
52 | string $url,
53 | string $eventName = '',
54 | array $params = [],
55 | ): void {
56 | InstantAnalytics::$plugin->ga4->addSimpleEvent($url, $eventName, $params);
57 |
58 | $this->redirect($url, 200);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
12 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/web/assets/dist/img/InstantAnalytics-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
12 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nystudio107/craft-instantanalytics-ga4",
3 | "description": "Instant Analytics brings full Google GA4 server-side analytics support to your Twig templates and automatic Craft Commerce integration",
4 | "type": "craft-plugin",
5 | "version": "5.0.1",
6 | "keywords": [
7 | "craft",
8 | "cms",
9 | "craftcms",
10 | "craft-plugin",
11 | "instant analytics",
12 | "google",
13 | "analytics",
14 | "server-side",
15 | "ga4"
16 | ],
17 | "support": {
18 | "docs": "https://nystudio107.com/docs/instant-analytics-ga4/",
19 | "issues": "https://nystudio107.com/plugins/instant-analytics-ga4/support",
20 | "source": "https://github.com/nystudio107/craft-instantanalytics-ga4"
21 | },
22 | "license": "proprietary",
23 | "authors": [
24 | {
25 | "name": "nystudio107",
26 | "homepage": "https://nystudio107.com"
27 | }
28 | ],
29 | "require": {
30 | "br33f/php-ga4-mp": "^0.1.0",
31 | "craftcms/cms": "^5.0.0",
32 | "nystudio107/craft-plugin-vite": "^5.0.0",
33 | "jaybizzle/crawler-detect": "^1.2.37"
34 | },
35 | "require-dev": {
36 | "craftcms/ecs": "dev-main",
37 | "craftcms/phpstan": "dev-main",
38 | "craftcms/rector": "dev-main",
39 | "craftcms/ckeditor": "^4.0.0",
40 | "craftcms/commerce": "^5.0.0",
41 | "craftcms/redactor": "^4.0.0",
42 | "nystudio107/craft-seomatic": "^5.0.0"
43 | },
44 | "scripts": {
45 | "phpstan": "phpstan --ansi --memory-limit=1G",
46 | "check-cs": "ecs check --ansi",
47 | "fix-cs": "ecs check --fix --ansi"
48 | },
49 | "config": {
50 | "allow-plugins": {
51 | "craftcms/plugin-installer": true,
52 | "yiisoft/yii2-composer": true
53 | },
54 | "optimize-autoloader": true,
55 | "sort-packages": true
56 | },
57 | "autoload": {
58 | "psr-4": {
59 | "nystudio107\\instantanalyticsGa4\\": "src/"
60 | }
61 | },
62 | "extra": {
63 | "class": "nystudio107\\instantanalyticsGa4\\InstantAnalytics",
64 | "handle": "instant-analytics-ga4",
65 | "name": "Instant Analytics GA4"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © nystudio107
2 |
3 | Permission is hereby granted to any person obtaining a copy of this software
4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies
5 | of the Software, and to permit persons to whom the Software is furnished to do
6 | so, subject to the following conditions:
7 |
8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be
9 | included in all copies or substantial portions of the Software.
10 |
11 | 2. **Don’t use the same license on more than one project.** Each licensed copy
12 | of the Software shall be actively installed in no more than one production
13 | environment at a time.
14 |
15 | 3. **Don’t mess with the licensing features.** Software features related to
16 | licensing shall not be altered or circumvented in any way, including (but
17 | not limited to) license validation, payment prompts, feature restrictions,
18 | and update eligibility.
19 |
20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice,
21 | prompt, reminder, or other message indicating that a payment is owed.
22 |
23 | 5. **Follow the law.** All use of the Software shall not violate any applicable
24 | law or regulation, nor infringe the rights of any other person or entity.
25 |
26 | Failure to comply with the foregoing conditions will automatically and
27 | immediately result in termination of the permission granted hereby. This
28 | license does not include any right to receive updates to the Software or
29 | technical support. Licensees bear all risk related to the quality and
30 | performance of the Software and any modifications made or obtained to it,
31 | including liability for actual and consequential harm, such as loss or
32 | corruption of data, and any necessary service, repair, or correction.
33 |
34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN
39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/?branch=v5) [](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/?branch=v5) [](https://scrutinizer-ci.com/g/nystudio107/craft-instantanalytics-ga4/build-status/v5) [](https://scrutinizer-ci.com/code-intelligence)
2 |
3 | # Instant Analytics GA4 plugin for Craft CMS 5.x
4 |
5 | Instant Analytics GA4 brings full Google Analytics support to your Twig templates and automatic Craft Commerce integration with Google Enhanced Ecommerce.
6 |
7 | **Note**: _This is an entirely rewritten plugin to reflect the new data model & API in GA4._
8 |
9 | 
10 |
11 | **Note**: _The license fee for this plugin is $59.00 via the Craft Plugin Store._
12 |
13 | ## Requirements
14 |
15 | This plugin requires Craft CMS 5.0.0 or later. Commerce 5 or later is required for Google Analytics Enhanced eCommerce support.
16 |
17 | ## Installation
18 |
19 | To install the plugin, follow these instructions.
20 |
21 | 1. Open your terminal and go to your Craft project:
22 |
23 | cd /path/to/project
24 |
25 | 2. Then tell Composer to load the plugin:
26 |
27 | composer require nystudio107/craft-instantanalytics-ga4
28 |
29 | 3. Install the plugin via `./craft install/plugin instant-analytics-ga4` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Instant Analytics.
30 |
31 | You can also install Instant Analytics via the **Plugin Store** in the Craft Control Panel.
32 |
33 | ## Documentation
34 |
35 | Click here -> [Instant Analytics GA4 Documentation](https://nystudio107.com/plugins/instant-analytics-ga4/documentation)
36 |
37 | ## Instant Analytics GA4 Roadmap
38 |
39 | Some things to do, and ideas for potential features:
40 |
41 | * Stable release
42 |
43 | Brought to you by [nystudio107](http://nystudio107.com)
44 |
--------------------------------------------------------------------------------
/src/templates/welcome.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {% extends 'instant-analytics-ga4/_layouts/instantanalytics-cp.twig' %}
3 |
4 | {% set title = 'Welcome to Instant Analytics!' %}
5 |
6 | {% set docsUrl = "https://github.com/nystudio107/craft-instantanalytics-ga4/" %}
7 | {% set linkGetStarted = url('settings/plugins/instant-analytics-ga4') %}
8 |
9 | {% do view.registerAssetBundle("nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsWelcomeAsset") %}
10 | {% set baseAssetsUrl = view.getAssetManager().getPublishedUrl('@nystudio107/instantanalyticsGa4/web/assets/dist', true) %}
11 |
12 | {% set crumbs = [
13 | { label: "Instant Analytics", url: url('instant-analytics') },
14 | { label: "Welcome"|t, url: url('instant-analytics-ga4/welcome') },
15 | ] %}
16 |
17 | {% block head %}
18 | {{ parent() }}
19 | {% set tagOptions = {
20 | 'depends': [
21 | 'nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsAsset'
22 | ],
23 | } %}
24 | {{ craft.instantAnalytics.register('src/js/welcome.ts', false, tagOptions, tagOptions) }}
25 | {% endblock %}
26 |
27 | {% block content %}
28 |
29 |
30 |
31 |
33 |
Thanks for using Instant Analytics GA4!
34 |
Instant Analytics GA4 brings full Google Analytics 4 support to your Twig templates and automatic Craft
35 | Commerce
36 | integration with Google Enhanced Ecommerce.
37 |
Instant Analytics GA4 also lets you track otherwise untrackable assets & events with Google Analytics 4, and
38 | eliminates the need for Javascript tracking.
39 |
40 |
For more information, please see the documentation .
41 |
42 |
43 |
44 |
45 |
46 |
47 | Get Started
48 |
49 |
50 |
51 |
56 | {% endblock %}
57 |
--------------------------------------------------------------------------------
/src/translations/en/instant-analytics.php:
--------------------------------------------------------------------------------
1 | '{name} plugin loaded',
25 | 'Craft Commerce is not installed' => 'Craft Commerce is not installed',
26 | 'Created sendPageView for: {eventCategory} - {eventAction} - {eventLabel} - {eventValue}' => 'Created sendPageView for: {eventCategory} - {eventAction} - {eventLabel} - {eventValue}',
27 | 'Created eventTrackingUrl for: {trackingUrl}' => 'Created eventTrackingUrl for: {trackingUrl}',
28 | 'Created pageViewTrackingUrl for: {trackingUrl}' => 'Created pageViewTrackingUrl for: {trackingUrl}',
29 | 'Analytics excluded for:: {requestIp} due to: `{setting}`' => 'Analytics excluded for:: {requestIp} due to: `{setting}`',
30 | 'Created sendPageView for: {url} - {title}' => 'Created sendPageView for: {url} - {title}',
31 | 'Created generic analytics object' => 'Created generic analytics object',
32 | 'Analytics not sent because googleAnalyticsTracking is not set' => 'Analytics not sent because googleAnalyticsTracking is not set',
33 | 'pageView sent, response:: {response}' => 'pageView sent, response:: {response}',
34 | 'addCommerceCheckoutStep step: `{step}` with option: `{option}`' => 'addCommerceCheckoutStep step: `{step}` with option: `{option}`',
35 | 'removeFromCart for `Commerce` - `Remove to Cart` - `{title}` - `{quantity}`' => 'removeFromCart for `Commerce` - `Remove to Cart` - `{title}` - `{quantity}`',
36 | 'Manifest file not found at: {manifestPath}' => 'Manifest file not found at: {manifestPath}',
37 | 'orderComplete for `Commerce` - `Purchase` - `{reference}` - `{price}`' => 'orderComplete for `Commerce` - `Purchase` - `{reference}` - `{price}`',
38 | 'addCommerceProductImpression for `{sku}` - `{name}` - `{name}` - `{index}`' => 'addCommerceProductImpression for `{sku}` - `{name}` - `{name}` - `{index}`',
39 | 'Module does not exist in the manifest: {moduleName}' => 'Module does not exist in the manifest: {moduleName}',
40 | 'addCommerceProductDetailView for `{sku}` - `{name} - `{name}`' => 'addCommerceProductDetailView for `{sku}` - `{name} - `{name}`',
41 | 'addToCart for `Commerce` - `Add to Cart` - `{title}` - `{quantity}`' => 'addToCart for `Commerce` - `Add to Cart` - `{title}` - `{quantity}`',
42 | 'orderComplete for `Commerce` - `Purchase` - `{number}` - `{price}`' => 'orderComplete for `Commerce` - `Purchase` - `{number}` - `{price}`',
43 | 'addCommerceProductDetailView for `{sku}` - `{name}`' => 'addCommerceProductDetailView for `{sku}` - `{name}`',
44 | ];
45 |
--------------------------------------------------------------------------------
/src/services/ServicesTrait.php:
--------------------------------------------------------------------------------
1 | = 8.2, and config() is called before __construct(),
38 | // so we can't extract it from the passed in $config
39 | $majorVersion = '4';
40 | // Dev server container name & port are based on the major version of this plugin
41 | $devPort = 3000 + (int)$majorVersion;
42 | $versionName = 'v' . $majorVersion;
43 | return [
44 | 'components' => [
45 | 'ga4' => Ga4::class,
46 | 'commerce' => CommerceService::class,
47 | // Register the vite service
48 | 'vite' => [
49 | 'assetClass' => InstantAnalyticsAsset::class,
50 | 'checkDevServer' => true,
51 | 'class' => VitePluginService::class,
52 | 'devServerInternal' => 'http://craft-instantanalytics-ga4-' . $versionName . '-buildchain-dev:' . $devPort,
53 | 'devServerPublic' => 'http://localhost:' . $devPort,
54 | 'errorEntry' => 'src/js/app.ts',
55 | 'useDevServer' => true,
56 | ],
57 | ],
58 | ];
59 | }
60 |
61 | // Public Methods
62 | // =========================================================================
63 |
64 | /**
65 | * Returns the GA4 service
66 | *
67 | * @return Ga4 The GA4 service
68 | * @throws InvalidConfigException
69 | */
70 | public function getGa4(): Ga4
71 | {
72 | return $this->get('ga4');
73 | }
74 |
75 | /**
76 | * Returns the commerce service
77 | *
78 | * @return CommerceService The commerce service
79 | * @throws InvalidConfigException
80 | */
81 | public function getCommerce(): CommerceService
82 | {
83 | return $this->get('commerce');
84 | }
85 |
86 | /**
87 | * Returns the vite service
88 | *
89 | * @return VitePluginService The vite service
90 | * @throws InvalidConfigException
91 | */
92 | public function getVite(): VitePluginService
93 | {
94 | return $this->get('vite');
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/variables/InstantAnalyticsVariable.php:
--------------------------------------------------------------------------------
1 | ga4->getPageViewEvent($url, $title);
51 | }
52 |
53 | /**
54 | * Get a simple event
55 | *
56 | * @param string $eventName
57 | * @return BaseEvent
58 | */
59 | public function simpleEvent(string $eventName = ''): BaseEvent
60 | {
61 | return InstantAnalytics::$plugin->ga4->getSimpleEvent($eventName);
62 | }
63 |
64 | /**
65 | * Return the GA4 Analytics object
66 | *
67 | * @return Analytics
68 | */
69 | public function ga4(): Analytics
70 | {
71 | return InstantAnalytics::$plugin->ga4->getAnalytics();
72 | }
73 |
74 | /**
75 | * @param Product|Variant $productVariant the Product or Variant
76 | */
77 | public function addCommerceProductView($productVariant): void
78 | {
79 | InstantAnalytics::$plugin->commerce->addCommerceProductImpression($productVariant);
80 | }
81 |
82 | /**
83 | * Get a PageView tracking URL
84 | *
85 | * @param $url
86 | * @param $title
87 | *
88 | * @return Markup
89 | * @throws Exception
90 | */
91 | public function pageViewTrackingUrl($url, $title): Markup
92 | {
93 | return Template::raw(AnalyticsHelper::getPageViewTrackingUrl($url, $title));
94 | }
95 |
96 | /**
97 | * Get an Event tracking URL
98 | *
99 | * @param string $url
100 | * @param string $eventName
101 | * @param array $params
102 | * @return Markup
103 | * @throws Exception
104 | */
105 | public function eventTrackingUrl(
106 | string $url,
107 | string $eventName = '',
108 | array $params = [],
109 | ): Markup {
110 | return Template::raw(AnalyticsHelper::getEventTrackingUrl($url, $eventName, $params));
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/ga4/ComponentFactory.php:
--------------------------------------------------------------------------------
1 | ItemParameter::class,
64 | 'AddPaymentInfoEvent' => AddPaymentInfoEvent::class,
65 | 'AddShippingInfoEvent' => AddShippingInfoEvent::class,
66 | 'AddToCartEvent' => AddToCartEvent::class,
67 | 'BaseEvent' => BaseEvent::class,
68 | 'BeginCheckoutEvent' => BeginCheckoutEvent::class,
69 | 'LoginEvent' => LoginEvent::class,
70 | 'PageViewEvent' => PageViewEvent::class,
71 | 'PurchaseEvent' => PurchaseEvent::class,
72 | 'RefundEvent' => RefundEvent::class,
73 | 'RemoveFromCartEvent' => RemoveFromCartEvent::class,
74 | 'SearchEvent' => SearchEvent::class,
75 | 'SelectItemEvent' => SelectItemEvent::class,
76 | 'SignUpEvent' => SignUpEvent::class,
77 | 'ViewCartEvent' => ViewCartEvent::class,
78 | 'ViewItemEvent' => ViewItemEvent::class,
79 | 'ViewItemListEvent' => ViewItemListEvent::class,
80 | 'ViewSearchResultsEvent' => ViewSearchResultsEvent::class,
81 | ];
82 |
83 | if (!array_key_exists($componentName, $componentMap)) {
84 | throw new \InvalidArgumentException(Craft::t('instant-analytics-ga4', 'Unknown event type - ' . $componentName));
85 | }
86 |
87 | return new $componentMap[$componentName]();
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/services/Ga4.php:
--------------------------------------------------------------------------------
1 | _analytics) {
45 | $this->_analytics = Craft::createObject(Analytics::class);
46 | $this->_analytics->init();
47 | }
48 |
49 | return $this->_analytics;
50 | }
51 |
52 | /**
53 | * Send a page view event
54 | */
55 | public function addPageViewEvent(string $url = '', string $pageTitle = ''): void
56 | {
57 | $request = Craft::$app->getRequest();
58 |
59 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest() && !$this->_pageViewSent) {
60 | $this->_pageViewSent = true;
61 |
62 | $pageView = $this->getPageViewEvent($url, !empty($pageTitle) ? $pageTitle : InstantAnalytics::$currentTemplate);
63 | $this->getAnalytics()->addEvent($pageView);
64 |
65 | InstantAnalytics::$plugin->logAnalyticsEvent(
66 | 'pageView event queued for sending',
67 | [],
68 | __METHOD__
69 | );
70 | }
71 | }
72 |
73 | /**
74 | * Add a basic event to be sent to GA4
75 | *
76 | * @param string $url
77 | * @param string $eventName
78 | * @param array $params
79 | */
80 | public function addSimpleEvent(string $url, string $eventName, array $params): void
81 | {
82 | $baseEvent = $this->getSimpleEvent($eventName);
83 | $baseEvent->setParamValue('documentPath', parse_url($url, PHP_URL_PATH));
84 |
85 | foreach ($params as $param => $value) {
86 | $baseEvent->addParam($param, new BaseParameter($value));
87 | }
88 |
89 | $this->getAnalytics()->addEvent($baseEvent);
90 |
91 | InstantAnalytics::$plugin->logAnalyticsEvent(
92 | 'Simple event queued for {url} with the following parameters {params}',
93 | ['url' => $url, 'params' => Json::encode($params)],
94 | __METHOD__
95 | );
96 | }
97 |
98 | /**
99 | * Create a page view event
100 | *
101 | * @param string $url
102 | * @param string $pageTitle
103 | * @return PageViewEvent
104 | */
105 | public function getPageViewEvent(string $url = '', string $pageTitle = ''): PageViewEvent
106 | {
107 | $event = $this->getAnalytics()->create()->PageViewEvent();
108 | $event->setPageTitle($pageTitle);
109 |
110 | // If SEOmatic is installed, set the page title from it
111 | $seomaticTitle = AnalyticsHelper::getTitleFromSeomatic();
112 |
113 | if ($seomaticTitle) {
114 | $event->setPageTitle($seomaticTitle);
115 | }
116 |
117 | $event->setPageLocation(AnalyticsHelper::getDocumentPathFromUrl($url));
118 |
119 | return $event;
120 | }
121 |
122 | /**
123 | * Create a simple event
124 | *
125 | * @param string $eventName
126 | * @return BaseEvent
127 | */
128 | public function getSimpleEvent(string $eventName): BaseEvent
129 | {
130 | $baseEvent = $this->getAnalytics()->create()->BaseEvent();
131 | $baseEvent->setName($eventName);
132 |
133 | return $baseEvent;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/config.php:
--------------------------------------------------------------------------------
1 | '',
30 |
31 | /**
32 | * The default Google Analytics measurement API secret used by GA4.
33 | */
34 | 'googleAnalyticsMeasurementApiSecret' => '',
35 |
36 | /**
37 | * Should the query string be stripped from the page tracking URL?
38 | */
39 | 'stripQueryString' => true,
40 |
41 | /**
42 | * Should page views be sent automatically when a page view happens?
43 | */
44 | 'autoSendPageView' => true,
45 |
46 | /**
47 | * If you plan to use Instant Analytics in conjunction with frontend JavaScript, this setting should be on, so that Instant Analytics requires a `clientId` from the frontend-set GA cookie before it will send analytics data.
48 | */
49 | 'requireGaCookieClientId' => true,
50 |
51 | /**
52 | * Should the GCLID cookie be created if it doesn't exist?
53 | */
54 | 'createGclidCookie' => true,
55 |
56 | /**
57 | * The field in a Commerce Product Variant that should be used for the category
58 | */
59 | 'productCategoryField' => '',
60 |
61 | /**
62 | * The field in a Commerce Product Variant that should be used for the brand
63 | */
64 | 'productBrandField' => '',
65 |
66 | /**
67 | * Whether add to cart events should be automatically sent
68 | */
69 | 'autoSendAddToCart' => true,
70 |
71 | /**
72 | * Whether remove from cart events should be automatically sent
73 | *
74 | * @var bool
75 | */
76 | 'autoSendRemoveFromCart' => true,
77 |
78 | /**
79 | * Whether purchase complete events should be automatically sent
80 | */
81 | 'autoSendPurchaseComplete' => true,
82 |
83 | /**
84 | * Controls whether Instant Analytics will send analytics data.
85 | */
86 | 'sendAnalyticsData' => true,
87 |
88 | /**
89 | * Controls whether Instant Analytics will send analytics data when `devMode` is on.
90 | */
91 | 'sendAnalyticsInDevMode' => true,
92 |
93 | /**
94 | * Controls whether we should filter out bot UserGents.
95 | */
96 | 'filterBotUserAgents' => true,
97 |
98 | /**
99 | * Controls whether we should exclude users logged into an admin account from Analytics tracking.
100 | */
101 | 'adminExclude' => false,
102 |
103 | /**
104 | * Controls whether analytics that blocked from being sent should be logged to
105 | * storage/logs/web.log
106 | * These are always logged if `devMode` is on
107 | */
108 | 'logExcludedAnalytics' => true,
109 |
110 | /**
111 | * Contains an array of Craft user group handles to exclude from Analytics tracking. If there's a match
112 | * for any of them, analytics data is not sent.
113 | */
114 | 'groupExcludes' => array(
115 | 'some_user_group_handle',
116 | ),
117 |
118 | /**
119 | * Contains an array of keys that correspond to $_SERVER[] super-global array keys to test against.
120 | * Each item in the sub-array is tested against the $_SERVER[] super-global key via RegEx; if there's
121 | * a match for any of them, analytics data is not sent. This allows you to filter based on whatever
122 | * information you want.
123 | * Reference: http://php.net/manual/en/reserved.variables.server.php
124 | * RegEx tester: http://regexr.com
125 | */
126 | 'serverExcludes' => array(
127 | 'REMOTE_ADDR' => array(
128 | '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/',
129 | ),
130 | ),
131 | ];
132 |
--------------------------------------------------------------------------------
/src/twigextensions/InstantAnalyticsTwigExtension.php:
--------------------------------------------------------------------------------
1 | getView();
57 | if ($view->getIsRenderingPageTemplate()) {
58 | $request = Craft::$app->getRequest();
59 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
60 | // Return our Analytics object as a Twig global
61 | $globals = [
62 | 'instantAnalytics' => InstantAnalytics::$plugin->ga4->getAnalytics(),
63 | ];
64 | }
65 | }
66 |
67 | return $globals;
68 | }
69 |
70 | /**
71 | * @inheritdoc
72 | */
73 | public function getFilters(): array
74 | {
75 | return [
76 | new TwigFilter('pageViewEvent', [$this, 'pageViewEvent']),
77 | new TwigFilter('simpleAnalyticsEvent', [$this, 'simpleEvent']),
78 | new TwigFilter('pageViewTrackingUrl', [$this, 'pageViewTrackingUrl']),
79 | new TwigFilter('eventTrackingUrl', [$this, 'eventTrackingUrl']),
80 | ];
81 | }
82 |
83 | /**
84 | * @inheritdoc
85 | */
86 | public function getFunctions(): array
87 | {
88 | return [
89 | new TwigFunction('pageViewEvent', [$this, 'pageViewEvent']),
90 | new TwigFunction('simpleAnalyticsEvent', [$this, 'simpleEvent']),
91 | new TwigFunction('pageViewTrackingUrl', [$this, 'pageViewTrackingUrl']),
92 | new TwigFunction('eventTrackingUrl', [$this, 'eventTrackingUrl']),
93 | ];
94 | }
95 |
96 | /**
97 | * Get a PageView analytics object
98 | *
99 | * @param string $url
100 | * @param string $title
101 | *
102 | * @return PageViewEvent object
103 | */
104 | public function pageViewEvent(string $url = '', string $title = ''): PageViewEvent
105 | {
106 | return InstantAnalytics::$plugin->ga4->getPageViewEvent($url, $title);
107 | }
108 |
109 | /**
110 | * Get an Event analytics object
111 | *
112 | * @param string $eventName
113 | * @return BaseEvent
114 | */
115 | public function simpleEvent(string $eventName = ''): BaseEvent
116 | {
117 | return InstantAnalytics::$plugin->ga4->getSimpleEvent($eventName);
118 | }
119 |
120 | /**
121 | * Get a PageView tracking URL
122 | *
123 | * @param $url
124 | * @param $title
125 | *
126 | * @return Markup
127 | * @throws Exception
128 | */
129 | public function pageViewTrackingUrl($url, $title): Markup
130 | {
131 | return Template::raw(Analytics::getPageViewTrackingUrl($url, $title));
132 | }
133 |
134 | /**
135 | * Get an Event tracking URL
136 | *
137 | * @param string $url
138 | * @param string $eventName
139 | * @param array $params
140 | * @return Markup
141 | * @throws Exception
142 | */
143 | public function eventTrackingUrl(
144 | string $url,
145 | string $eventName,
146 | array $params = [],
147 | ): Markup {
148 | return Template::raw(Analytics::getEventTrackingUrl($url, $eventName, $params));
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/templates/settings.twig:
--------------------------------------------------------------------------------
1 | {#
2 | /**
3 | * Instant Analytics plugin for Craft CMS
4 | *
5 | * Instant Analytics Settings.twig
6 | *
7 | * @author nystudio107
8 | * @copyright Copyright (c) 2017 nystudio107
9 | * @link https://nystudio107.com
10 | * @package InstantAnalytics
11 | * @since 1.0.0
12 | */
13 | #}
14 |
15 | {% import "_includes/forms" as forms %}
16 |
17 | {% do view.registerAssetBundle("nystudio107\\instantanalyticsGa4\\assetbundles\\instantanalytics\\InstantAnalyticsAsset") %}
18 |
19 | {% set commerce = craft.app.plugins.getPlugin('commerce', false) %}
20 | {% set commerceInstalled = craft.app.plugins.isPluginInstalled('commerce') %}
21 | {% set commerceEnabled = craft.app.plugins.isPluginEnabled('commerce') %}
22 |
23 | {% set commerceEnabled = (commerce and commerceEnabled and commerceInstalled) %}
24 |
25 | {{ forms.autosuggestField({
26 | label: 'Google Analytics Measurement ID:',
27 | instructions: "Enter your Google Analytics Measurement ID here to be used by the GA4 API. Only enter the ID, e.g.: G-XXXXXXXXXX, not the entire script code.",
28 | suggestEnvVars: true,
29 | id: 'googleAnalyticsMeasurementId',
30 | name: 'googleAnalyticsMeasurementId',
31 | value: settings['googleAnalyticsMeasurementId'],
32 | }) }}
33 |
34 | {{ forms.autosuggestField({
35 | label: 'Google Analytics Measurement API Secret:',
36 | instructions: "Enter your Google Analytics Measurement API secret here, to be used by GA4 API.",
37 | suggestEnvVars: true,
38 | id: 'googleAnalyticsMeasurementApiSecret',
39 | name: 'googleAnalyticsMeasurementApiSecret',
40 | value: settings['googleAnalyticsMeasurementApiSecret'],
41 | }) }}
42 |
43 | {{ forms.lightswitchField({
44 | label: 'Strip Query String from PageView URLs:',
45 | instructions: "If this setting is on, the query string will be stripped from PageView URLs before being sent to Google Analytics. e.g.: `/some/path?token=1235312` would be sent as just `/some/path`",
46 | id: 'stripQueryString',
47 | name: 'stripQueryString',
48 | on: settings['stripQueryString']}) }}
49 |
50 | {{ forms.lightswitchField({
51 | label: 'Auto Send PageViews:',
52 | instructions: "If this setting is on, a PageView will automatically be sent to Google after a every page is rendered. If it is off, you'll need to send it manually using `{% hook 'iaSendPageView' %}`",
53 | id: 'autoSendPageView',
54 | name: 'autoSendPageView',
55 | on: settings['autoSendPageView']}) }}
56 |
57 | {{ forms.lightswitchField({
58 | label: 'Create GCLID Cookie:',
59 | instructions: "Google Click Identifier (GCLID), is a unique tracking parameter that Google uses to transfer information between your Google Ads account and your Google Analytics account. If this setting is on, the GCLID will be created if it doesn't exist for the current request.",
60 | id: 'createGclidCookie',
61 | name: 'createGclidCookie',
62 | on: settings['createGclidCookie']}) }}
63 |
64 | {{ forms.lightswitchField({
65 | label: 'Auto Send "Add To Cart" Events:',
66 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent when an item is added to your Craft Commerce cart.",
67 | id: 'autoSendAddToCart',
68 | name: 'autoSendAddToCart',
69 | disabled: (not commerceEnabled),
70 | on: settings['autoSendAddToCart']}) }}
71 |
72 | {{ forms.lightswitchField({
73 | label: 'Auto Send "Remove From Cart" Events:',
74 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent when an item is removed from your Craft Commerce cart.",
75 | id: 'autoSendRemoveFromCart',
76 | name: 'autoSendRemoveFromCart',
77 | disabled: (not commerceEnabled),
78 | on: settings['autoSendRemoveFromCart']}) }}
79 |
80 | {{ forms.lightswitchField({
81 | label: 'Auto Send "Purchase Complete" Events:',
82 | instructions: "If this setting is on, Google Analytics Enhanced Ecommerce events are automatically sent a purchase is completed.",
83 | id: 'autoSendPurchaseComplete',
84 | name: 'autoSendPurchaseComplete',
85 | disabled: (not commerceEnabled),
86 | on: settings['autoSendPurchaseComplete']}) }}
87 |
88 | {{ forms.selectField({
89 | label: 'Commerce Product Category Field:',
90 | instructions: "Choose the field in your Product or Variant field layout that should be used for the product's Category field for Google Analytics Enhanced Ecommerce",
91 | id: 'productCategoryField',
92 | name: 'productCategoryField',
93 | options: commerceFields,
94 | disabled: (not commerceEnabled),
95 | value: settings['productCategoryField'],
96 | }) }}
97 |
98 | {{ forms.selectField({
99 | label: 'Commerce Product Brand Field:',
100 | instructions: "Choose the field in your Product or Variant field layout that should be used for the product's Brand field for Google Analytics Enhanced Ecommerce",
101 | id: 'productBrandField',
102 | name: 'productBrandField',
103 | options: commerceFields,
104 | disabled: (not commerceEnabled),
105 | value: settings['productBrandField'],
106 | }) }}
107 |
--------------------------------------------------------------------------------
/src/models/Settings.php:
--------------------------------------------------------------------------------
1 | [
179 | '/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/',
180 | ],
181 | ];
182 |
183 | // Public Methods
184 | // =========================================================================
185 |
186 | /**
187 | * @return array
188 | */
189 | public function defineRules(): array
190 | {
191 | return [
192 | [
193 | [
194 | 'stripQueryString',
195 | 'autoSendPageView',
196 | 'requireGaCookieClientId',
197 | 'createGclidCookie',
198 | 'autoSendAddToCart',
199 | 'autoSendRemoveFromCart',
200 | 'autoSendPurchaseComplete',
201 | 'sendAnalyticsData',
202 | 'sendAnalyticsInDevMode',
203 | 'filterBotUserAgents',
204 | 'adminExclude',
205 | 'logExcludedAnalytics',
206 | ],
207 | 'boolean',
208 | ],
209 | [
210 | [
211 | 'googleAnalyticsTracking',
212 | 'productCategoryField',
213 | 'productBrandField',
214 | 'googleAnalyticsTracking',
215 | ],
216 | 'string',
217 | ],
218 | [
219 | [
220 | 'groupExcludes',
221 | 'serverExcludes',
222 | ],
223 | ArrayValidator::class,
224 | ],
225 | ];
226 | }
227 |
228 | /**
229 | * @return array
230 | */
231 | public function behaviors(): array
232 | {
233 | return [
234 | 'typecast' => [
235 | 'class' => AttributeTypecastBehavior::class,
236 | // 'attributeTypes' will be composed automatically according to `rules()`
237 | ],
238 | 'parser' => [
239 | 'class' => EnvAttributeParserBehavior::class,
240 | 'attributes' => [
241 | 'googleAnalyticsTracking',
242 | ],
243 | ],
244 | ];
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/src/helpers/Field.php:
--------------------------------------------------------------------------------
1 | [
48 | CKEditorField::class,
49 | PlainTextField::class,
50 | RedactorField::class,
51 | TagsField::class,
52 | CategoriesField::class,
53 | EntriesField::class,
54 | ],
55 | self::ASSET_FIELD_CLASS_KEY => [
56 | AssetsField::class,
57 | ],
58 | self::BLOCK_FIELD_CLASS_KEY => [
59 | MatrixField::class,
60 | ],
61 | ];
62 |
63 | // Static Methods
64 | // =========================================================================
65 |
66 | /**
67 | * Return all the fields from the $layout that are of the type
68 | * $fieldClassKey
69 | *
70 | * @param string $fieldClassKey
71 | * @param FieldLayout $layout
72 | * @param bool $keysOnly
73 | *
74 | * @return array
75 | */
76 | public static function fieldsOfTypeFromLayout(
77 | string $fieldClassKey,
78 | FieldLayout $layout,
79 | bool $keysOnly = true,
80 | ): array {
81 | $foundFields = [];
82 | if (!empty(self::FIELD_CLASSES[$fieldClassKey])) {
83 | $fieldClasses = self::FIELD_CLASSES[$fieldClassKey];
84 | $fields = $layout->getCustomFields();
85 | /** @var BaseField $field */
86 | foreach ($fields as $field) {
87 | /** @var array $fieldClasses */
88 | foreach ($fieldClasses as $fieldClass) {
89 | if ($field instanceof $fieldClass) {
90 | $foundFields[$field->handle] = $field->name;
91 | }
92 | }
93 | }
94 | }
95 |
96 | // Return only the keys if asked
97 | if ($keysOnly) {
98 | $foundFields = array_keys($foundFields);
99 | }
100 |
101 | return $foundFields;
102 | }
103 |
104 | /**
105 | * Return all of the fields in the $element of the type $fieldClassKey
106 | *
107 | * @param Element $element
108 | * @param string $fieldClassKey
109 | * @param bool $keysOnly
110 | *
111 | * @return array
112 | */
113 | public static function fieldsOfTypeFromElement(
114 | Element $element,
115 | string $fieldClassKey,
116 | bool $keysOnly = true,
117 | ): array {
118 | $foundFields = [];
119 | $layout = $element->getFieldLayout();
120 | if ($layout !== null) {
121 | $foundFields = self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly);
122 | }
123 |
124 | return $foundFields;
125 | }
126 |
127 | /**
128 | * Return all of the fields from Users layout of the type $fieldClassKey
129 | *
130 | * @param string $fieldClassKey
131 | * @param bool $keysOnly
132 | *
133 | * @return array
134 | */
135 | public static function fieldsOfTypeFromUsers(string $fieldClassKey, bool $keysOnly = true): array
136 | {
137 | $layout = Craft::$app->getFields()->getLayoutByType(User::class);
138 |
139 | return self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly);
140 | }
141 |
142 | /**
143 | * Return all the fields from all Asset Volume layouts of the type
144 | * $fieldClassKey
145 | *
146 | * @param string $fieldClassKey
147 | * @param bool $keysOnly
148 | *
149 | * @return array
150 | */
151 | public static function fieldsOfTypeFromAssetVolumes(string $fieldClassKey, bool $keysOnly = true): array
152 | {
153 | $foundFields = [];
154 | $volumes = Craft::$app->getVolumes()->getAllVolumes();
155 | foreach ($volumes as $volume) {
156 | /** @var Volume $volume */
157 | try {
158 | $layout = $volume->getFieldLayout();
159 | } catch (Exception $e) {
160 | $layout = null;
161 | }
162 | if ($layout) {
163 | /** @noinspection SlowArrayOperationsInLoopInspection */
164 | $foundFields = array_merge(
165 | $foundFields,
166 | self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly)
167 | );
168 | }
169 | }
170 |
171 | return $foundFields;
172 | }
173 |
174 | /**
175 | * Return all the fields from all Global Set layouts of the type
176 | * $fieldClassKey
177 | *
178 | * @param string $fieldClassKey
179 | * @param bool $keysOnly
180 | *
181 | * @return array
182 | */
183 | public static function fieldsOfTypeFromGlobals(string $fieldClassKey, bool $keysOnly = true): array
184 | {
185 | $foundFields = [];
186 | $globals = Craft::$app->getGlobals()->getAllSets();
187 | foreach ($globals as $global) {
188 | $layout = $global->getFieldLayout();
189 | /** @phpstan-ignore-next-line */
190 | if ($layout) {
191 | $fields = self::fieldsOfTypeFromLayout($fieldClassKey, $layout, $keysOnly);
192 | // Prefix the keys with the global set name
193 | $prefix = $global->handle;
194 | $fields = array_combine(
195 | array_map(static function($key) use ($prefix) {
196 | return $prefix . '.' . $key;
197 | }, array_keys($fields)),
198 | $fields
199 | );
200 | // Merge with any fields we've already found
201 | /** @noinspection SlowArrayOperationsInLoopInspection */
202 | $foundFields = array_merge(
203 | $foundFields,
204 | $fields
205 | );
206 | }
207 | }
208 |
209 | return $foundFields;
210 | }
211 |
212 | /**
213 | * Return all of the fields in the $matrixEntry of the type $fieldType class
214 | *
215 | * @param Entry $matrixEntry
216 | * @param string $fieldType
217 | * @param bool $keysOnly
218 | *
219 | * @return array
220 | */
221 | public static function matrixFieldsOfType(Entry $matrixEntry, string $fieldType, bool $keysOnly = true): array
222 | {
223 | $foundFields = [];
224 |
225 | try {
226 | $matrixEntryTypeModel = $matrixEntry->getType();
227 | } catch (InvalidConfigException $e) {
228 | $matrixEntryTypeModel = null;
229 | }
230 | if ($matrixEntryTypeModel) {
231 | $fields = $matrixEntryTypeModel->getCustomFields();
232 | /** @var BaseField $field */
233 | foreach ($fields as $field) {
234 | if ($field instanceof $fieldType) {
235 | $foundFields[$field->handle] = $field->name;
236 | }
237 | }
238 | // Return only the keys if asked
239 | if ($keysOnly) {
240 | $foundFields = array_keys($foundFields);
241 | }
242 | }
243 |
244 | return $foundFields;
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/src/helpers/Analytics.php:
--------------------------------------------------------------------------------
1 | renderEnabled) {
40 | return null;
41 | }
42 | $titleTag = Seomatic::$plugin->title->get('title');
43 |
44 | if ($titleTag === null) {
45 | return null;
46 | }
47 |
48 | $titleArray = $titleTag->renderAttributes();
49 |
50 | if (empty($titleArray['title'])) {
51 | return null;
52 | }
53 |
54 | return $titleArray['title'];
55 | }
56 |
57 | /**
58 | * Return a sanitized documentPath from a URL
59 | *
60 | * @param string $url
61 | *
62 | * @return string
63 | */
64 | public static function getDocumentPathFromUrl(string $url = ''): string
65 | {
66 | if ($url === '') {
67 | $url = Craft::$app->getRequest()->getFullPath();
68 | }
69 |
70 | // We want to send just a path to GA for page views
71 | if (UrlHelper::isAbsoluteUrl($url)) {
72 | $urlParts = parse_url($url);
73 | $url = $urlParts['path'] ?? '/';
74 | if (isset($urlParts['query'])) {
75 | $url .= '?' . $urlParts['query'];
76 | }
77 | }
78 |
79 | // We don't want to send protocol-relative URLs either
80 | if (UrlHelper::isProtocolRelativeUrl($url)) {
81 | $url = substr($url, 1);
82 | }
83 |
84 | // Strip the query string if that's the global config setting
85 | if (InstantAnalytics::$settings) {
86 | if (InstantAnalytics::$settings->stripQueryString !== null
87 | && InstantAnalytics::$settings->stripQueryString) {
88 | $url = UrlHelper::stripQueryString($url);
89 | }
90 | }
91 |
92 | // We always want the path to be / rather than empty
93 | if ($url === '') {
94 | $url = '/';
95 | }
96 |
97 | return $url;
98 | }
99 |
100 | /**
101 | * Get a PageView tracking URL
102 | *
103 | * @param $url
104 | * @param $title
105 | *
106 | * @return string
107 | * @throws Exception
108 | */
109 | public static function getPageViewTrackingUrl($url, $title): string
110 | {
111 | $urlParams = compact('url', 'title');
112 |
113 | $path = parse_url($url, PHP_URL_PATH);
114 | $pathFragments = explode('/', rtrim($path, '/'));
115 | $fileName = end($pathFragments);
116 | $trackingUrl = UrlHelper::siteUrl('instantanalytics/pageViewTrack/' . $fileName, $urlParams);
117 |
118 | InstantAnalytics::$plugin->logAnalyticsEvent(
119 | 'Created pageViewTrackingUrl for: {trackingUrl}',
120 | [
121 | 'trackingUrl' => $trackingUrl,
122 | ],
123 | __METHOD__
124 | );
125 |
126 | return $trackingUrl;
127 | }
128 |
129 | /**
130 | * Get an Event tracking URL
131 | *
132 | * @param string $url
133 | * @param string $eventName
134 | * @param array $params
135 | * @return string
136 | * @throws Exception
137 | */
138 | public static function getEventTrackingUrl(
139 | string $url,
140 | string $eventName,
141 | array $params = [],
142 | ): string {
143 | $urlParams = compact('url', 'eventName', 'params');
144 |
145 | $fileName = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME);
146 | $trackingUrl = UrlHelper::siteUrl('instantanalytics/eventTrack/' . $fileName, $urlParams);
147 |
148 | InstantAnalytics::$plugin->logAnalyticsEvent(
149 | 'Created eventTrackingUrl for: {trackingUrl}',
150 | [
151 | 'trackingUrl' => $trackingUrl,
152 | ],
153 | __METHOD__
154 | );
155 |
156 | return $trackingUrl;
157 | }
158 |
159 | /**
160 | * _shouldSendAnalytics determines whether we should be sending Google
161 | * Analytics data
162 | *
163 | * @return bool
164 | */
165 | public static function shouldSendAnalytics(): bool
166 | {
167 | $result = true;
168 | $request = Craft::$app->getRequest();
169 |
170 | $logExclusion = static function(string $setting) {
171 | if (InstantAnalytics::$settings->logExcludedAnalytics) {
172 | $request = Craft::$app->getRequest();
173 | $requestIp = $request->getUserIP();
174 | InstantAnalytics::$plugin->logAnalyticsEvent(
175 | 'Analytics excluded for:: {requestIp} due to: `{setting}`',
176 | compact('requestIp', 'setting'),
177 | __METHOD__
178 | );
179 | }
180 | };
181 |
182 | if (!InstantAnalytics::$settings->sendAnalyticsData) {
183 | $logExclusion('sendAnalyticsData');
184 | return false;
185 | }
186 |
187 | if (!InstantAnalytics::$settings->sendAnalyticsInDevMode && Craft::$app->getConfig()->getGeneral()->devMode) {
188 | $logExclusion('sendAnalyticsInDevMode');
189 | return false;
190 | }
191 |
192 | if ($request->getIsConsoleRequest()) {
193 | $logExclusion('Craft::$app->getRequest()->getIsConsoleRequest()');
194 | return false;
195 | }
196 |
197 | if ($request->getIsCpRequest()) {
198 | $logExclusion('Craft::$app->getRequest()->getIsCpRequest()');
199 | return false;
200 | }
201 |
202 | if ($request->getIsLivePreview()) {
203 | $logExclusion('Craft::$app->getRequest()->getIsLivePreview()');
204 | return false;
205 | }
206 |
207 | // Check the $_SERVER[] super-global exclusions
208 | if (InstantAnalytics::$settings->serverExcludes !== null
209 | && !empty(InstantAnalytics::$settings->serverExcludes)) {
210 | foreach (InstantAnalytics::$settings->serverExcludes as $match => $matchArray) {
211 | if (isset($_SERVER[$match])) {
212 | foreach ($matchArray as $matchItem) {
213 | if (preg_match($matchItem, $_SERVER[$match])) {
214 | $logExclusion('serverExcludes');
215 |
216 | return false;
217 | }
218 | }
219 | }
220 | }
221 | }
222 |
223 | // Filter out bot/spam requests via UserAgent
224 | if (InstantAnalytics::$settings->filterBotUserAgents) {
225 | $crawlerDetect = new CrawlerDetect();
226 | // Check the user agent of the current 'visitor'
227 | if ($crawlerDetect->isCrawler()) {
228 | $logExclusion('filterBotUserAgents');
229 |
230 | return false;
231 | }
232 | }
233 |
234 | // Filter by user group
235 | $userService = Craft::$app->getUser();
236 | /** @var ?UserElement $user */
237 | $user = $userService->getIdentity();
238 | if ($user) {
239 | if (InstantAnalytics::$settings->adminExclude && $user->admin) {
240 | $logExclusion('adminExclude');
241 |
242 | return false;
243 | }
244 |
245 | if (InstantAnalytics::$settings->groupExcludes !== null
246 | && !empty(InstantAnalytics::$settings->groupExcludes)) {
247 | foreach (InstantAnalytics::$settings->groupExcludes as $matchItem) {
248 | if ($user->isInGroup($matchItem)) {
249 | $logExclusion('groupExcludes');
250 |
251 | return false;
252 | }
253 | }
254 | }
255 | }
256 |
257 | return $result;
258 | }
259 |
260 | /**
261 | * getClientId handles the parsing of the _ga cookie or setting it to a
262 | * unique identifier
263 | *
264 | * @return string the cid
265 | */
266 | public static function getClientId(): string
267 | {
268 | $cid = '';
269 | if (isset($_COOKIE['_ga'])) {
270 | $parts = explode(".", $_COOKIE['_ga'], 4);
271 | if ($parts !== false) {
272 | $cid = implode('.', array_slice($parts, 2));
273 | }
274 | } elseif (isset($_COOKIE['_ia']) && $_COOKIE['_ia'] !== '') {
275 | $cid = $_COOKIE['_ia'];
276 | } else {
277 | // Generate our own client id, otherwise.
278 | $cid = static::gaGenUUID() . '.1';
279 | }
280 |
281 | if (InstantAnalytics::$settings->createGclidCookie && !empty($cid)) {
282 | setcookie('_ia', $cid, strtotime('+2 years'), '/'); // Two years
283 | }
284 |
285 | return $cid;
286 | }
287 |
288 | /**
289 | * Get the Google Analytics session string from the cookie.
290 | *
291 | * @return string
292 | */
293 | public static function getSessionString(): string
294 | {
295 | $sessionString = '';
296 | $measurementId = App::parseEnv(InstantAnalytics::$settings->googleAnalyticsMeasurementId);
297 | $cookieName = '_ga_' . StringHelper::removeLeft($measurementId, 'G-');
298 |
299 | if (isset($_COOKIE[$cookieName])) {
300 | $parts = explode(".", $_COOKIE[$cookieName], 5);
301 | if ($parts !== false) {
302 | $sessionString = implode('.', array_slice($parts, 2, 2));
303 | }
304 | }
305 |
306 | return $sessionString;
307 | }
308 |
309 | /**
310 | * Get the user id.
311 | *
312 | * @return string
313 | */
314 | public static function getUserId(): string
315 | {
316 | $userId = Craft::$app->getUser()->getId();
317 |
318 | if (!$userId) {
319 | return '';
320 | }
321 |
322 | return $userId;
323 | }
324 |
325 | /**
326 | * gaGenUUID Generate UUID v4 function - needed to generate a CID when one
327 | * isn't available
328 | *
329 | * @return string The generated UUID
330 | */
331 | protected static function gaGenUUID()
332 | {
333 | return sprintf(
334 | '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
335 | // 32 bits for "time_low"
336 | mt_rand(0, 0xffff),
337 | mt_rand(0, 0xffff),
338 | // 16 bits for "time_mid"
339 | mt_rand(0, 0xffff),
340 | // 16 bits for "time_hi_and_version",
341 | // four most significant bits holds version number 4
342 | mt_rand(0, 0x0fff) | 0x4000,
343 | // 16 bits, 8 bits for "clk_seq_hi_res",
344 | // 8 bits for "clk_seq_low",
345 | // two most significant bits holds zero and one for variant DCE1.1
346 | mt_rand(0, 0x3fff) | 0x8000,
347 | // 48 bits for "node"
348 | mt_rand(0, 0xffff),
349 | mt_rand(0, 0xffff),
350 | mt_rand(0, 0xffff)
351 | );
352 | }
353 | }
354 |
--------------------------------------------------------------------------------
/src/ga4/Analytics.php:
--------------------------------------------------------------------------------
1 | _sessionString === null) {
102 | $this->_sessionString = AnalyticsHelper::getSessionString();
103 | }
104 |
105 | if (str_contains($this->_sessionString, '.')) {
106 | [$sessionId, $sessionNumber] = explode('.', $this->_sessionString);
107 | $event->setParamValue('sessionId', $sessionId);
108 | $event->setParamValue('sessionNumber', $sessionNumber);
109 | }
110 |
111 | $this->eventList[] = $event;
112 | }
113 |
114 | /**
115 | * Send the events collected so far.
116 | *
117 | * @return ?array
118 | * @throws HydrationException
119 | * @throws ValidationException
120 | */
121 | public function sendCollectedEvents(): ?array
122 | {
123 | if ($this->_shouldSendAnalytics === null) {
124 | $this->_shouldSendAnalytics = AnalyticsHelper::shouldSendAnalytics();
125 | }
126 |
127 | if (!$this->_shouldSendAnalytics) {
128 | return null;
129 | }
130 |
131 | $service = $this->service();
132 |
133 | if (!$service) {
134 | return null;
135 | }
136 |
137 | $request = $this->request();
138 | $eventCount = count($this->eventList);
139 |
140 | if (!InstantAnalytics::$settings->sendAnalyticsData) {
141 | InstantAnalytics::$plugin->logAnalyticsEvent(
142 | 'Analytics not enabled - skipped sending {count} events',
143 | ['count' => $eventCount],
144 | __METHOD__
145 | );
146 |
147 | return null;
148 | }
149 |
150 | if ($eventCount === 0) {
151 | InstantAnalytics::$plugin->logAnalyticsEvent(
152 | 'No events collected to send',
153 | [],
154 | __METHOD__
155 | );
156 |
157 | return null;
158 | }
159 |
160 | InstantAnalytics::$plugin->logAnalyticsEvent(
161 | 'Sending {count} analytics events',
162 | ['count' => $eventCount],
163 | __METHOD__
164 | );
165 |
166 | // Batch into groups of 25
167 | $responses = [];
168 |
169 | foreach (array_chunk($this->eventList, 25) as $chunk) {
170 | $request->getEvents()->setEventList([]);
171 |
172 | /** @var AbstractEvent $event */
173 | foreach ($chunk as $event) {
174 | $request->addEvent($event);
175 | }
176 |
177 | $responses[] = $service->send($request);
178 | }
179 |
180 |
181 | return $responses;
182 | }
183 |
184 | public function getAffiliation(): ?string
185 | {
186 | return $this->_affiliation;
187 | }
188 |
189 | /**
190 | * Set affiliation for all the events that incorporate Commerce Product info for the remaining duration of request.
191 | *
192 | * @param string $affiliation
193 | * @return $this
194 | */
195 | public function setAffiliation(string $affiliation): self
196 | {
197 | $this->_affiliation = $affiliation;
198 | return $this;
199 | }
200 |
201 | /**
202 | * Add a commerce item list impression.
203 | *
204 | * @param Product|Variant $productVariant
205 | * @param int $index
206 | * @param string $listName
207 | * @throws InvalidConfigException
208 | */
209 | public function addCommerceProductImpression(Product|Variant $productVariant, int $index = 0, string $listName = 'default')
210 | {
211 | InstantAnalytics::$plugin->commerce->addCommerceProductImpression($productVariant);
212 | }
213 |
214 | /**
215 | * Begin checkout.
216 | *
217 | * @param Order $cart
218 | */
219 | public function beginCheckout(Order $cart)
220 | {
221 | InstantAnalytics::$plugin->commerce->triggerBeginCheckoutEvent($cart);
222 | }
223 |
224 | /**
225 | * Add a commerce item list impression.
226 | *
227 | * @param Product|Variant $productVariant
228 | * @param int $index
229 | * @param string $listName
230 | * @throws InvalidConfigException
231 | * @deprecated `Analytics::addCommerceProductDetailView()` is deprecated. Use `Analytics::addCommerceProductImpression()` instead.
232 | */
233 | public function addCommerceProductDetailView(Product|Variant $productVariant, int $index = 0, string $listName = 'default')
234 | {
235 | Craft::$app->getDeprecator()->log('Analytics::addCommerceProductDetailView()', '`Analytics::addCommerceProductDetailView()` is deprecated. Use `Analytics::addCommerceProductImpression()` instead.');
236 | $this->addCommerceProductImpression($productVariant);
237 | }
238 |
239 | /**
240 | * Add a commerce product list impression.
241 | *
242 | * @param array $products
243 | * @param $listName
244 | */
245 | public function addCommerceProductListImpression(array $products, string $listName = 'default')
246 | {
247 | InstantAnalytics::$plugin->commerce->addCommerceProductListImpression($products, $listName);
248 | }
249 |
250 | /**
251 | * Set the measurement id.
252 | *
253 | * @param string $measurementId
254 | * @return $this
255 | */
256 | public function setMeasurementId(string $measurementId): self
257 | {
258 | $this->service()?->setMeasurementId($measurementId);
259 | return $this;
260 | }
261 |
262 | /**
263 | * Set the API secret.
264 | *
265 | * @param string $apiSecret
266 | * @return $this
267 | */
268 | public function setApiSecret(string $apiSecret): self
269 | {
270 | $this->service()?->setApiSecret($apiSecret);
271 | return $this;
272 | }
273 |
274 | public function __call(string $methodName, array $arguments): ?self
275 | {
276 | $knownProperties = [
277 | 'allowGoogleSignals' => 'allow_google_signals',
278 | 'allowAdPersonalizationSignals' => 'allow_ad_personalization_signals',
279 | 'campaignContent' => 'campaign_content',
280 | 'campaignId' => 'campaign_id',
281 | 'campaignMedium' => 'campaign_medium',
282 | 'campaignName' => 'campaign_name',
283 | 'campaignSource' => 'campaign_source',
284 | 'campaignTerm' => 'campaign_term',
285 | 'campaign' => 'campaign',
286 | 'clientId' => 'client_id',
287 | 'contentGroup' => 'content_group',
288 | 'cookieDomain' => 'cookie_domain',
289 | 'cookieExpires' => 'cookie_expires',
290 | 'cookieFlags' => 'cookie_flags',
291 | 'cookiePath' => 'cookie_path',
292 | 'cookiePrefix' => 'cookie_prefix',
293 | 'cookieUpdate' => 'cookie_update',
294 | 'language' => 'language',
295 | 'pageLocation' => 'page_location',
296 | 'pageReferrer' => 'page_referrer',
297 | 'pageTitle' => 'page_title',
298 | 'sendPageView' => 'send_page_view',
299 | 'screenResolution' => 'screen_resolution',
300 | 'userId' => 'user_id',
301 | ];
302 |
303 | if (str_starts_with($methodName, 'set')) {
304 | $methodName = lcfirst(substr($methodName, 3));
305 |
306 | $service = $this->service();
307 | if ($service && !empty($knownProperties[$methodName])) {
308 | $service->setAdditionalQueryParam($knownProperties[$methodName], $arguments[0]);
309 |
310 | return $this;
311 | }
312 | }
313 |
314 | return null;
315 | }
316 |
317 | public function request(): BaseRequest
318 | {
319 | if ($this->_request === null) {
320 | $this->_request = new BaseRequest();
321 |
322 | $this->_request->setClientId(AnalyticsHelper::getClientId());
323 |
324 | if (InstantAnalytics::$settings->sendUserId) {
325 | $userId = AnalyticsHelper::getUserId();
326 |
327 | if ($userId) {
328 | $this->request()->setUserId($userId);
329 | }
330 | }
331 | }
332 |
333 |
334 | return $this->_request;
335 | }
336 |
337 | /**
338 | * Init the service used to send events
339 | */
340 | public function init(): void
341 | {
342 | $this->service();
343 | $this->request();
344 | }
345 |
346 | protected function service(): ?Service
347 | {
348 | if ($this->_service === null) {
349 | $settings = InstantAnalytics::$settings;
350 | $apiSecret = App::parseEnv($settings->googleAnalyticsMeasurementApiSecret);
351 | $measurementId = App::parseEnv($settings->googleAnalyticsMeasurementId);
352 |
353 | if (empty($apiSecret) || empty($measurementId)) {
354 | InstantAnalytics::$plugin->logAnalyticsEvent(
355 | 'API secret or measurement ID not set up for Instant Analytics',
356 | [],
357 | __METHOD__
358 | );
359 | $this->_service = false;
360 |
361 | return null;
362 | }
363 | $this->_service = new Service($apiSecret, $measurementId);
364 |
365 | $ga4Client = new HttpClient();
366 | $ga4Client->setClient(Craft::createGuzzleClient());
367 | $this->_service->setHttpClient($ga4Client);
368 |
369 | $request = Craft::$app->getRequest();
370 | try {
371 | $session = Craft::$app->getSession();
372 | } catch (MissingComponentException $exception) {
373 | $session = null;
374 | }
375 |
376 | $this->setPageReferrer($request->getReferrer());
377 |
378 | // Load any campaign values from session or request
379 | $campaignParams = [
380 | 'utm_source' => 'CampaignSource',
381 | 'utm_medium' => 'CampaignMedium',
382 | 'utm_campaign' => 'CampaignName',
383 | 'utm_content' => 'CampaignContent',
384 | 'utm_term' => 'CampaignTerm',
385 | ];
386 |
387 | // Load them up for GA4
388 | foreach ($campaignParams as $key => $method) {
389 | $value = $request->getParam($key) ?? $session->get($key) ?? null;
390 | $method = 'set' . $method;
391 |
392 | $this->$method($value);
393 |
394 | if ($session && $value) {
395 | $session->set($key, $value);
396 | }
397 | }
398 |
399 | // If SEOmatic is installed, set the affiliation as well
400 | if (InstantAnalytics::$seomaticPlugin && Seomatic::$settings->renderEnabled && Seomatic::$plugin->metaContainers->metaSiteVars !== null) {
401 | $siteName = Seomatic::$plugin->metaContainers->metaSiteVars->siteName;
402 | $this->setAffiliation($siteName);
403 | }
404 | }
405 |
406 | if ($this->_service === false) {
407 | return null;
408 | }
409 |
410 | return $this->_service;
411 | }
412 | }
413 |
--------------------------------------------------------------------------------
/src/InstantAnalytics.php:
--------------------------------------------------------------------------------
1 | getSettings();
130 | self::$settings = $settings;
131 |
132 | // Add in our Craft components
133 | $this->addComponents();
134 | // Install our global event handlers
135 | $this->installEventListeners();
136 |
137 | Craft::info(
138 | Craft::t(
139 | 'instant-analytics-ga4',
140 | '{name} plugin loaded',
141 | ['name' => $this->name]
142 | ),
143 | __METHOD__
144 | );
145 | }
146 |
147 | /**
148 | * Handle the `{% hook iaSendPageView %}`
149 | */
150 | public function iaSendPageView(/** @noinspection PhpUnusedParameterInspection */ array &$context = []): string
151 | {
152 | $this->ga4->addPageViewEvent();
153 | return '';
154 | }
155 |
156 | /**
157 | * Handle the `{% hook iaInsertGtag %}`
158 | */
159 | public function iaInsertGtag(/** @noinspection PhpUnusedParameterInspection */ array &$context = []): string
160 | {
161 | $config = [];
162 |
163 | if (self::$settings->sendUserId) {
164 | $userId = Analytics::getUserId();
165 | if (!empty($userId)) {
166 | $config['user_id'] = $userId;
167 | }
168 | }
169 |
170 | $measurementId = App::parseEnv(self::$settings->googleAnalyticsMeasurementId);
171 |
172 |
173 | $view = Craft::$app->getView();
174 | $existingMode = $view->getTemplateMode();
175 | $view->setTemplateMode(View::TEMPLATE_MODE_CP);
176 | $content = $view->renderTemplate('instant-analytics-ga4/_includes/gtag', compact('measurementId', 'config'));
177 | $view->setTemplateMode($existingMode);
178 |
179 | return $content;
180 | }
181 |
182 | public function logAnalyticsEvent(string $message, array $variables = [], string $category = ''): void
183 | {
184 | Craft::info(
185 | Craft::t('instant-analytics-ga4', $message, $variables),
186 | $category
187 | );
188 | }
189 |
190 | /**
191 | * @inheritdoc
192 | */
193 | protected function settingsHtml(): ?string
194 | {
195 | $commerceFields = [];
196 |
197 | if (self::$commercePlugin !== null) {
198 | $productTypes = self::$commercePlugin->getProductTypes()->getAllProductTypes();
199 |
200 | foreach ($productTypes as $productType) {
201 | $productFields = $this->getPullFieldsFromLayoutId($productType->fieldLayoutId);
202 | /** @noinspection SlowArrayOperationsInLoopInspection */
203 | $commerceFields = array_merge($commerceFields, $productFields);
204 | if ($productType->maxVariants > 1) {
205 | $variantFields = $this->getPullFieldsFromLayoutId($productType->variantFieldLayoutId);
206 | /** @noinspection SlowArrayOperationsInLoopInspection */
207 | $commerceFields = array_merge($commerceFields, $variantFields);
208 | }
209 | }
210 | }
211 |
212 | // Rend the settings template
213 | try {
214 | return Craft::$app->getView()->renderTemplate(
215 | 'instant-analytics-ga4/settings',
216 | [
217 | 'settings' => $this->getSettings(),
218 | 'commerceFields' => $commerceFields,
219 | ]
220 | );
221 | } catch (Exception $exception) {
222 | Craft::error($exception->getMessage(), __METHOD__);
223 | }
224 |
225 | return '';
226 | }
227 | // Protected Methods
228 | // =========================================================================
229 |
230 | /**
231 | * Add in our Craft components
232 | */
233 | protected function addComponents(): void
234 | {
235 | $view = Craft::$app->getView();
236 | // Add in our Twig extensions
237 | $view->registerTwigExtension(new InstantAnalyticsTwigExtension());
238 | // Install our template hooks
239 | $view->hook('iaSendPageView', [$this, 'iaSendPageView']);
240 | $view->hook('iaInsertGtag', [$this, 'iaInsertGtag']);
241 |
242 | // Register our variables
243 | Event::on(
244 | CraftVariable::class,
245 | CraftVariable::EVENT_INIT,
246 | function(Event $event): void {
247 | /** @var CraftVariable $variable */
248 | $variable = $event->sender;
249 | $variable->set('instantAnalytics', [
250 | 'class' => InstantAnalyticsVariable::class,
251 | 'viteService' => $this->vite,
252 | ]);
253 | }
254 | );
255 | }
256 |
257 | /**
258 | * Install our event listeners
259 | */
260 | protected function installEventListeners(): void
261 | {
262 | // Handler: Plugins::EVENT_AFTER_INSTALL_PLUGIN
263 | Event::on(
264 | Plugins::class,
265 | Plugins::EVENT_AFTER_INSTALL_PLUGIN,
266 | function(PluginEvent $event): void {
267 | if ($event->plugin === $this) {
268 | $request = Craft::$app->getRequest();
269 | if ($request->isCpRequest) {
270 | Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('instant-analytics-ga4/welcome'))->send();
271 | }
272 | }
273 | }
274 | );
275 |
276 | // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
277 | Event::on(
278 | Plugins::class,
279 | Plugins::EVENT_AFTER_LOAD_PLUGINS,
280 | function() {
281 | // Determine if Craft Commerce is installed & enabled
282 | /** @var Commerce $commercePlugin */
283 | $commercePlugin = Craft::$app->getPlugins()->getPlugin(self::COMMERCE_PLUGIN_HANDLE);
284 | self::$commercePlugin = $commercePlugin;
285 | // Determine if SEOmatic is installed & enabled
286 | /** @var Seomatic $seomaticPlugin */
287 | $seomaticPlugin = Craft::$app->getPlugins()->getPlugin(self::SEOMATIC_PLUGIN_HANDLE);
288 | self::$seomaticPlugin = $seomaticPlugin;
289 |
290 | // Make sure to install these only after we definitely know whether other plugins are installed
291 | $request = Craft::$app->getRequest();
292 | // Install only for non-console site requests
293 | if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
294 | $this->installSiteEventListeners();
295 | }
296 |
297 | // Install only for non-console Control Panel requests
298 | if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
299 | $this->installCpEventListeners();
300 | }
301 | }
302 | );
303 | }
304 |
305 | /**
306 | * Install site event listeners for site requests only
307 | */
308 | protected function installSiteEventListeners(): void
309 | {
310 | // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
311 | Event::on(
312 | UrlManager::class,
313 | UrlManager::EVENT_REGISTER_SITE_URL_RULES,
314 | function(RegisterUrlRulesEvent $event): void {
315 | Craft::debug(
316 | 'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
317 | __METHOD__
318 | );
319 | // Register our Control Panel routes
320 | $event->rules = array_merge(
321 | $event->rules,
322 | $this->customFrontendRoutes()
323 | );
324 | }
325 | );
326 | // Remember the name of the currently rendering template
327 | Event::on(
328 | View::class,
329 | View::EVENT_BEFORE_RENDER_PAGE_TEMPLATE,
330 | static function(TemplateEvent $event): void {
331 | self::$currentTemplate = $event->template;
332 | }
333 | );
334 | // Send the page-view event.
335 | Event::on(
336 | View::class,
337 | View::EVENT_AFTER_RENDER_PAGE_TEMPLATE,
338 | function(TemplateEvent $event): void {
339 | if (self::$settings->autoSendPageView) {
340 | $request = Craft::$app->getRequest();
341 | if (!$request->getIsAjax()) {
342 | $this->ga4->addPageViewEvent();
343 | }
344 | }
345 | }
346 | );
347 |
348 | // Send the collected events
349 | Event::on(
350 | Response::class,
351 | Response::EVENT_BEFORE_SEND,
352 | function(Event $event): void {
353 | // Initialize this sooner rather than later, since it's possible this will want to tinker with cookies
354 | $this->ga4->getAnalytics();
355 | }
356 | );
357 |
358 | // Send the collected events
359 | Event::on(
360 | Response::class,
361 | Response::EVENT_AFTER_SEND,
362 | function(Event $event): void {
363 | $this->ga4->getAnalytics()->sendCollectedEvents();
364 | }
365 | );
366 |
367 | // Commerce-specific hooks
368 | if (self::$commercePlugin !== null) {
369 | Event::on(Order::class, Order::EVENT_AFTER_COMPLETE_ORDER, function(Event $e): void {
370 | $order = $e->sender;
371 | if (self::$settings->autoSendPurchaseComplete) {
372 | $this->commerce->triggerOrderCompleteEvent($order);
373 | }
374 | });
375 |
376 | Event::on(Order::class, Order::EVENT_AFTER_ADD_LINE_ITEM, function(LineItemEvent $e): void {
377 | $lineItem = $e->lineItem;
378 | if (self::$settings->autoSendAddToCart) {
379 | $this->commerce->triggerAddToCartEvent($lineItem);
380 | }
381 | });
382 |
383 | // Check to make sure Order::EVENT_AFTER_REMOVE_LINE_ITEM is defined
384 | if (defined(Order::class . '::EVENT_AFTER_REMOVE_LINE_ITEM')) {
385 | Event::on(Order::class, Order::EVENT_AFTER_REMOVE_LINE_ITEM, function(LineItemEvent $e): void {
386 | $lineItem = $e->lineItem;
387 | if (self::$settings->autoSendRemoveFromCart) {
388 | $this->commerce->triggerRemoveFromCartEvent($lineItem);
389 | }
390 | });
391 | }
392 | }
393 | }
394 |
395 | /**
396 | * Install site event listeners for Control Panel requests only
397 | */
398 | protected function installCpEventListeners(): void
399 | {
400 | }
401 |
402 | /**
403 | * Return the custom frontend routes
404 | *
405 | * @return array
406 | */
407 | protected function customFrontendRoutes(): array
408 | {
409 | return [
410 | 'instantanalytics/pageViewTrack' =>
411 | 'instant-analytics-ga4/track/track-page-view-url',
412 | 'instantanalytics/eventTrack/?' =>
413 | 'instant-analytics-ga4/track/track-event-url',
414 | ];
415 | }
416 |
417 | /**
418 | * @inheritdoc
419 | */
420 | protected function createSettingsModel(): ?Model
421 | {
422 | return new Settings();
423 | }
424 |
425 | // Private Methods
426 | // =========================================================================
427 |
428 | /**
429 | * @param $layoutId
430 | *
431 | * @return mixed[]|array
432 | */
433 | private function getPullFieldsFromLayoutId($layoutId): array
434 | {
435 | $result = ['' => 'none'];
436 | if ($layoutId === null) {
437 | return $result;
438 | }
439 |
440 | $fieldLayout = Craft::$app->getFields()->getLayoutById($layoutId);
441 | if ($fieldLayout) {
442 | $result = FieldHelper::fieldsOfTypeFromLayout(FieldHelper::TEXT_FIELD_CLASS_KEY, $fieldLayout, false);
443 | }
444 |
445 | return $result;
446 | }
447 | }
448 |
--------------------------------------------------------------------------------
/src/services/Commerce.php:
--------------------------------------------------------------------------------
1 | ga4->getAnalytics()->create()->PurchaseEvent();
50 | $this->addCommerceOrderToEvent($event, $order);
51 |
52 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event);
53 |
54 | InstantAnalytics::$plugin->logAnalyticsEvent(
55 | 'Adding `Commerce - Order Complete event`: `{reference}` => `{price}`',
56 | ['reference' => $order->reference, 'price' => $order->totalPrice],
57 | __METHOD__
58 | );
59 | }
60 | }
61 |
62 | /**
63 | * Enqueue analytics information for a new checkout flow
64 | *
65 | * @param ?Order $order
66 | */
67 | public function triggerBeginCheckoutEvent(Order $order = null)
68 | {
69 | if ($order) {
70 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->BeginCheckoutEvent();
71 | // First, include the transaction data
72 | $event->setCurrency($order->getPaymentCurrency())
73 | ->setValue($order->getTotalPrice());
74 |
75 | // Add each line item in the cart
76 | $index = 1;
77 | foreach ($order->lineItems as $lineItem) {
78 | $this->addProductDataFromLineItem($event, $lineItem, $index);
79 | $index++;
80 | }
81 |
82 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event);
83 |
84 | InstantAnalytics::$plugin->logAnalyticsEvent(
85 | 'Adding `Commerce - Begin Checkout event``',
86 | [],
87 | __METHOD__
88 | );
89 | }
90 | }
91 |
92 | /**
93 | * Send analytics information for the item added to the cart
94 | *
95 | * @param LineItem $lineItem the line item that was added
96 | */
97 | public function triggerAddToCartEvent(LineItem $lineItem): void
98 | {
99 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->AddToCartEvent();
100 | $this->addProductDataFromLineItem($event, $lineItem);
101 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event);
102 |
103 | InstantAnalytics::$plugin->logAnalyticsEvent(
104 | 'Adding `Commerce - Add to Cart event`: `{title}` => `{quantity}`',
105 | ['title' => $lineItem->purchasable->title ?? $lineItem->getDescription(), 'quantity' => $lineItem->qty],
106 | __METHOD__
107 | );
108 | }
109 |
110 | /**
111 | * Send analytics information for the item removed from the cart
112 | *
113 | * @param LineItem $lineItem
114 | */
115 | public function triggerRemoveFromCartEvent(LineItem $lineItem)
116 | {
117 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->RemoveFromCartEvent();
118 | $this->addProductDataFromLineItem($event, $lineItem);
119 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event);
120 |
121 | InstantAnalytics::$plugin->logAnalyticsEvent(
122 | 'Adding `Commerce - Remove from Cart event`: `{title}` => `{quantity}`',
123 | ['title' => $lineItem->purchasable->title ?? $lineItem->getDescription(), 'quantity' => $lineItem->qty],
124 | __METHOD__
125 | );
126 | }
127 |
128 | /**
129 | * Add a product impression from a Craft Commerce Product or Variant
130 | *
131 | * @param Product|Variant|null $productVariant the Product or Variant
132 | * @throws InvalidConfigException
133 | */
134 | public function addCommerceProductImpression(Variant|Product|null $productVariant): void
135 | {
136 | if ($productVariant) {
137 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->ViewItemEvent();
138 | $this->addProductDataFromProductOrVariant($event, $productVariant);
139 |
140 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event);
141 |
142 | $sku = $productVariant instanceof Product ? $productVariant->getDefaultVariant()->sku : $productVariant->sku;
143 | $name = $productVariant instanceof Product ? $productVariant->getName() : $productVariant->getProduct()->getName();
144 | InstantAnalytics::$plugin->logAnalyticsEvent(
145 | 'Adding view item event for `{sku}` - `{name}` - `{name}` - `{index}`',
146 | ['sku' => $sku, 'name' => $name],
147 | __METHOD__
148 | );
149 | }
150 | }
151 |
152 | /**
153 | * Add a product list impression from a Craft Commerce Product or Variant list
154 | *
155 | * @param Product[]|Variant[] $products
156 | * @param string $listName
157 | */
158 | public function addCommerceProductListImpression(array $products, string $listName = 'default'): void
159 | {
160 | if (!empty($products)) {
161 | $event = InstantAnalytics::$plugin->ga4->getAnalytics()->create()->ViewItemListEvent();
162 | foreach ($products as $index => $productVariant) {
163 | $this->addProductDataFromProductOrVariant($event, $productVariant, $index, $listName);
164 | }
165 |
166 | InstantAnalytics::$plugin->ga4->getAnalytics()->addEvent($event);
167 |
168 | InstantAnalytics::$plugin->logAnalyticsEvent(
169 | 'Adding view item list event. Listing {number} of items from the `{listName}` list.',
170 | ['number' => count($products), 'listName' => $listName],
171 | __METHOD__
172 | );
173 | }
174 | }
175 |
176 | /**
177 | * Add a Craft Commerce OrderModel to a Purchase Event
178 | *
179 | * @param PurchaseEvent $event The PurchaseEvent
180 | * @param Order $order
181 | */
182 | protected function addCommerceOrderToEvent(PurchaseEvent $event, Order $order)
183 | {
184 | // First, include the transaction data
185 | $event->setCurrency($order->getPaymentCurrency())
186 | ->setTransactionId($order->reference)
187 | ->setValue($order->getTotalPrice())
188 | ->setTax($order->getTotalTax())
189 | ->setShipping($order->getTotalShippingCost());
190 |
191 | // Coupon code
192 | if ($order->couponCode) {
193 | $event->setCoupon($order->couponCode);
194 | }
195 |
196 | // Add each line item in the transaction
197 | // Two cases - variant and non variant products
198 | $index = 1;
199 |
200 | foreach ($order->lineItems as $lineItem) {
201 | $this->addProductDataFromLineItem($event, $lineItem, $index);
202 | $index++;
203 | }
204 | }
205 |
206 | /**
207 | * Add a Craft Commerce LineItem to an Analytics object
208 | *
209 | * @param ItemBaseEvent $event
210 | * @param LineItem $lineItem
211 | * @param int $index
212 | * @param string $listName
213 | *
214 | * @return string the title of the product
215 | * @throws InvalidConfigException
216 | */
217 | protected function addProductDataFromLineItem(ItemBaseEvent $event, LineItem $lineItem, int $index = 0, string $listName = ''): string
218 | {
219 | $eventItem = $this->getNewItemParameter();
220 |
221 | $product = null;
222 | $purchasable = $lineItem->purchasable;
223 |
224 | /** @phpstan-ignore-next-line */
225 | if ($purchasable === null) {
226 | $eventItem->setItemName($lineItem->getDescription());
227 | $eventItem->setItemId($lineItem->getSku());
228 | } else {
229 | $eventItem->setItemName($purchasable->title ?? $lineItem->getDescription());
230 | $eventItem->setItemId($purchasable->getSku());
231 | }
232 | $eventItem->setPrice($lineItem->salePrice);
233 | $eventItem->setQuantity($lineItem->qty);
234 |
235 | // Handle this purchasable being a Variant
236 | if (is_a($purchasable, Variant::class)) {
237 | /** @var Variant $purchasable */
238 | $product = $purchasable->getProduct();
239 | $variant = $purchasable;
240 | // Product with variants
241 | $eventItem->setItemName($product->title);
242 | $eventItem->setItemVariant($variant->title);
243 | $eventItem->setItemCategory($product->getType());
244 | }
245 |
246 | // Handle this purchasable being a Product
247 | if (is_a($purchasable, Product::class)) {
248 | /** @var Product $purchasable */
249 | $product = $purchasable;
250 | $eventItem->setItemName($product->title);
251 | $eventItem->setItemVariant($product->title);
252 | $eventItem->setItemCategory($product->getType());
253 | }
254 |
255 | // Handle product lists
256 | if ($index) {
257 | $eventItem->setIndex($index);
258 | }
259 |
260 | if ($listName) {
261 | $eventItem->setItemListName($listName);
262 | }
263 |
264 | // Add in any custom categories/brands that might be set
265 | if (InstantAnalytics::$settings && $product) {
266 | if (isset(InstantAnalytics::$settings['productCategoryField'])
267 | && !empty(InstantAnalytics::$settings['productCategoryField'])) {
268 | $category = $this->pullDataFromField(
269 | $product,
270 | InstantAnalytics::$settings['productCategoryField']
271 | );
272 | $eventItem->setItemCategory($category);
273 | }
274 | if (isset(InstantAnalytics::$settings['productBrandField'])
275 | && !empty(InstantAnalytics::$settings['productBrandField'])) {
276 | $brand = $this->pullDataFromField(
277 | $product,
278 | InstantAnalytics::$settings['productBrandField']
279 | );
280 |
281 | $eventItem->setItemBrand($brand);
282 | }
283 | }
284 |
285 | //Add each product to the hit to be sent
286 | $event->addItem($eventItem);
287 |
288 | return $eventItem->getItemName();
289 | }
290 |
291 | /**
292 | * Extract product data from a Craft Commerce Product or Variant
293 | *
294 | * @param Product|Variant|null $productVariant the Product or Variant
295 | *
296 | * @throws InvalidConfigException
297 | */
298 | protected function addProductDataFromProductOrVariant(ItemBaseEvent $event, $productVariant = null, $index = null, $listName = ''): void
299 | {
300 | if ($productVariant === null) {
301 | return;
302 | }
303 |
304 | $eventItem = $this->getNewItemParameter();
305 |
306 | $isVariant = $productVariant instanceof Variant;
307 | $variant = $isVariant ? $productVariant : $productVariant->getDefaultVariant();
308 |
309 | if (!$variant) {
310 | return;
311 | }
312 |
313 | $eventItem->setItemId($variant->sku);
314 | $eventItem->setItemName($variant->title);
315 | $eventItem->setPrice((float)number_format($variant->price, 2, '.', ''));
316 |
317 | $category = ($isVariant ? $variant->getProduct() : $productVariant)->getType()['name'];
318 |
319 | if (InstantAnalytics::$settings) {
320 | if (isset(InstantAnalytics::$settings['productCategoryField'])
321 | && !empty(InstantAnalytics::$settings['productCategoryField'])) {
322 | $category = $this->pullDataFromField(
323 | $productVariant,
324 | InstantAnalytics::$settings['productCategoryField']
325 | );
326 | /* @TODO not sure what this even does
327 | * if (empty($productData['category']) && $isVariant) {
328 | * $category = $this->pullDataFromField(
329 | * $productVariant->product,
330 | * InstantAnalytics::$settings['productCategoryField']
331 | * );
332 | * }
333 | */
334 | }
335 | $eventItem->setItemCategory($category);
336 |
337 | if (isset(InstantAnalytics::$settings['productBrandField'])
338 | && !empty(InstantAnalytics::$settings['productBrandField'])) {
339 | $brand = $this->pullDataFromField(
340 | $productVariant,
341 | InstantAnalytics::$settings['productBrandField'],
342 | true
343 | );
344 | /* @TODO not sure what this even does
345 | * if (empty($productData['brand']) && $isVariant) {
346 | * $brand = $this->pullDataFromField(
347 | * $productVariant,
348 | * InstantAnalytics::$settings['productBrandField'],
349 | * true
350 | * );
351 | * }
352 | */
353 | $eventItem->setItemBrand($brand);
354 | }
355 | }
356 |
357 | if ($index !== null) {
358 | $eventItem->setIndex($index);
359 | }
360 |
361 | if (!empty($listName)) {
362 | $eventItem->setItemListName($listName);
363 | }
364 |
365 | // Add item info to the event
366 | $event->addItem($eventItem);
367 | }
368 |
369 | /**
370 | * @param Product|Variant|null $productVariant
371 | * @param string $fieldHandle
372 | * @param bool $isBrand
373 | *
374 | * @return string
375 | */
376 | protected function pullDataFromField($productVariant, $fieldHandle, $isBrand = false): string
377 | {
378 | $result = '';
379 | if ($productVariant && $fieldHandle) {
380 | $srcField = $productVariant[$fieldHandle] ?? $productVariant->product[$fieldHandle] ?? null;
381 | // Handle eager loaded elements
382 | if (is_array($srcField)) {
383 | return $this->getDataFromElements($isBrand, $srcField);
384 | }
385 | // If the source field isn't an object, return nothing
386 | if (!is_object($srcField)) {
387 | return $result;
388 | }
389 | switch (get_class($srcField)) {
390 | case TagQuery::class:
391 | break;
392 | case CategoryQuery::class:
393 | case EntryQuery::class:
394 | $result = $this->getDataFromElements($isBrand, $srcField->all());
395 | break;
396 |
397 | default:
398 | $result = strip_tags($srcField->__toString());
399 | break;
400 | }
401 | }
402 |
403 | return $result;
404 | }
405 |
406 | /**
407 | * @param bool $isBrand
408 | * @param array $elements
409 | * @return string
410 | */
411 | protected function getDataFromElements(bool $isBrand, array $elements): string
412 | {
413 | $cats = [];
414 |
415 | if ($isBrand) {
416 | // Because we can only have one brand, we'll get
417 | // the very last category. This means if our
418 | // brand is a sub-category, we'll get the child
419 | // not the parent.
420 | foreach ($elements as $cat) {
421 | $cats = [$cat->title];
422 | }
423 | } else {
424 | // For every category, show its ancestors
425 | // delimited by a slash.
426 | foreach ($elements as $cat) {
427 | $name = $cat->title;
428 |
429 | while ($cat = $cat->parent) {
430 | $name = $cat->title . '/' . $name;
431 | }
432 |
433 | $cats[] = $name;
434 | }
435 | }
436 |
437 | // Join separate categories with a pipe.
438 | return implode('|', $cats);
439 | }
440 |
441 | /**
442 | * Create an item parameter and set affiliation on it, if any exists.
443 | *
444 | * @return ItemParameter
445 | */
446 | protected function getNewItemParameter(): ItemParameter
447 | {
448 | $parameter = new ItemParameter();
449 | $parameter->setAffiliation(InstantAnalytics::$plugin->ga4->getAnalytics()->getAffiliation());
450 | $parameter->setCurrency(CommercePlugin::getInstance()->getPaymentCurrencies()->getPrimaryPaymentCurrencyIso());
451 |
452 | return $parameter;
453 | }
454 | }
455 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/welcome-BP_XNmLM.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @vue/shared v3.5.5
3 | * (c) 2018-present Yuxi (Evan) You and Vue contributors
4 | * @license MIT
5 | **//*! #__NO_SIDE_EFFECTS__ */function ws(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const V={},Ye=[],ve=()=>{},Di=()=>!1,Vt=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Es=e=>e.startsWith("onUpdate:"),J=Object.assign,Ss=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Hi=Object.prototype.hasOwnProperty,D=(e,t)=>Hi.call(e,t),I=Array.isArray,ct=e=>Ut(e)==="[object Map]",Li=e=>Ut(e)==="[object Set]",P=e=>typeof e=="function",q=e=>typeof e=="string",et=e=>typeof e=="symbol",z=e=>e!==null&&typeof e=="object",In=e=>(z(e)||P(e))&&P(e.then)&&P(e.catch),ji=Object.prototype.toString,Ut=e=>ji.call(e),Ni=e=>Ut(e).slice(8,-1),$i=e=>Ut(e)==="[object Object]",Cs=e=>q(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ft=ws(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Wt=e=>{const t=Object.create(null);return s=>t[s]||(t[s]=e(s))},Bi=/-(\w)/g,Ke=Wt(e=>e.replace(Bi,(t,s)=>s?s.toUpperCase():"")),Vi=/\B([A-Z])/g,Ge=Wt(e=>e.replace(Vi,"-$1").toLowerCase()),An=Wt(e=>e.charAt(0).toUpperCase()+e.slice(1)),kt=Wt(e=>e?`on${An(e)}`:""),ze=(e,t)=>!Object.is(e,t),es=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},Ui=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let ks;const Rn=()=>ks||(ks=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Ts(e){if(I(e)){const t={};for(let s=0;s{if(s){const n=s.split(Ki);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function Os(e){let t="";if(q(e))t=e;else if(I(e))for(let s=0;s0)return;let e;for(;ut;){let t=ut;for(ut=void 0;t;){const s=t.nextEffect;if(t.nextEffect=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function jn(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Nn(e){let t,s=e.depsTail,n=s;for(;n;){const i=n.prevDep;n.version===-1?(n===s&&(s=i),Is(n),Zi(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=i}e.deps=t,e.depsTail=s}function as(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&$n(t.dep.computed)||t.dep.version!==t.version)return!0;return!!e._dirty}function $n(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===gt))return;e.globalVersion=gt;const t=e.dep;if(e.flags|=2,t.version>0&&!e.isSSR&&!as(e)){e.flags&=-3;return}const s=N,n=ae;N=e,ae=!0;try{jn(e);const i=e.fn(e._value);(t.version===0||ze(i,e._value))&&(e._value=i,t.version++)}catch(i){throw t.version++,i}finally{N=s,ae=n,Nn(e),e.flags&=-3}}function Is(e){const{dep:t,prevSub:s,nextSub:n}=e;if(s&&(s.nextSub=n,e.prevSub=void 0),n&&(n.prevSub=s,e.nextSub=void 0),t.subs===e&&(t.subs=s),!t.subs&&t.computed){t.computed.flags&=-5;for(let i=t.computed.deps;i;i=i.nextDep)Is(i)}}function Zi(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}let ae=!0;const Bn=[];function He(){Bn.push(ae),ae=!1}function Le(){const e=Bn.pop();ae=e===void 0?!0:e}function en(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=N;N=void 0;try{t()}finally{N=s}}}let gt=0;class Qi{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Vn{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0}track(t){if(!N||!ae||N===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==N)s=this.activeLink=new Qi(N,this),N.deps?(s.prevDep=N.depsTail,N.depsTail.nextDep=s,N.depsTail=s):N.deps=N.depsTail=s,N.flags&4&&Un(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=N.depsTail,s.nextDep=void 0,N.depsTail.nextDep=s,N.depsTail=s,N.deps===s&&(N.deps=n)}return s}trigger(t){this.version++,gt++,this.notify(t)}notify(t){Ms();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()}finally{Ps()}}}function Un(e){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)Un(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}const hs=new WeakMap,We=Symbol(""),ds=Symbol(""),mt=Symbol("");function X(e,t,s){if(ae&&N){let n=hs.get(e);n||hs.set(e,n=new Map);let i=n.get(s);i||n.set(s,i=new Vn),i.track()}}function Te(e,t,s,n,i,r){const l=hs.get(e);if(!l){gt++;return}const f=u=>{u&&u.trigger()};if(Ms(),t==="clear")l.forEach(f);else{const u=I(e),d=u&&Cs(s);if(u&&s==="length"){const a=Number(n);l.forEach((p,y)=>{(y==="length"||y===mt||!et(y)&&y>=a)&&f(p)})}else switch(s!==void 0&&f(l.get(s)),d&&f(l.get(mt)),t){case"add":u?d&&f(l.get("length")):(f(l.get(We)),ct(e)&&f(l.get(ds)));break;case"delete":u||(f(l.get(We)),ct(e)&&f(l.get(ds)));break;case"set":ct(e)&&f(l.get(We));break}}Ps()}function qe(e){const t=j(e);return t===e?t:(X(t,"iterate",mt),xe(e)?t:t.map(ce))}function As(e){return X(e=j(e),"iterate",mt),e}const ki={__proto__:null,[Symbol.iterator](){return ss(this,Symbol.iterator,ce)},concat(...e){return qe(this).concat(...e.map(t=>I(t)?qe(t):t))},entries(){return ss(this,"entries",e=>(e[1]=ce(e[1]),e))},every(e,t){return we(this,"every",e,t,void 0,arguments)},filter(e,t){return we(this,"filter",e,t,s=>s.map(ce),arguments)},find(e,t){return we(this,"find",e,t,ce,arguments)},findIndex(e,t){return we(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return we(this,"findLast",e,t,ce,arguments)},findLastIndex(e,t){return we(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return we(this,"forEach",e,t,void 0,arguments)},includes(...e){return ns(this,"includes",e)},indexOf(...e){return ns(this,"indexOf",e)},join(e){return qe(this).join(e)},lastIndexOf(...e){return ns(this,"lastIndexOf",e)},map(e,t){return we(this,"map",e,t,void 0,arguments)},pop(){return rt(this,"pop")},push(...e){return rt(this,"push",e)},reduce(e,...t){return tn(this,"reduce",e,t)},reduceRight(e,...t){return tn(this,"reduceRight",e,t)},shift(){return rt(this,"shift")},some(e,t){return we(this,"some",e,t,void 0,arguments)},splice(...e){return rt(this,"splice",e)},toReversed(){return qe(this).toReversed()},toSorted(e){return qe(this).toSorted(e)},toSpliced(...e){return qe(this).toSpliced(...e)},unshift(...e){return rt(this,"unshift",e)},values(){return ss(this,"values",ce)}};function ss(e,t,s){const n=As(e),i=n[t]();return n!==e&&!xe(e)&&(i._next=i.next,i.next=()=>{const r=i._next();return r.value&&(r.value=s(r.value)),r}),i}const er=Array.prototype;function we(e,t,s,n,i,r){const l=As(e),f=l!==e&&!xe(e),u=l[t];if(u!==er[t]){const p=u.apply(e,r);return f?ce(p):p}let d=s;l!==e&&(f?d=function(p,y){return s.call(this,ce(p),y,e)}:s.length>2&&(d=function(p,y){return s.call(this,p,y,e)}));const a=u.call(l,d,n);return f&&i?i(a):a}function tn(e,t,s,n){const i=As(e);let r=s;return i!==e&&(xe(e)?s.length>3&&(r=function(l,f,u){return s.call(this,l,f,u,e)}):r=function(l,f,u){return s.call(this,l,ce(f),u,e)}),i[t](r,...n)}function ns(e,t,s){const n=j(e);X(n,"iterate",mt);const i=n[t](...s);return(i===-1||i===!1)&&Ls(s[0])?(s[0]=j(s[0]),n[t](...s)):i}function rt(e,t,s=[]){He(),Ms();const n=j(e)[t].apply(e,s);return Ps(),Le(),n}const tr=ws("__proto__,__v_isRef,__isVue"),Wn=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(et));function sr(e){et(e)||(e=String(e));const t=j(this);return X(t,"has",e),t.hasOwnProperty(e)}class Kn{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){const i=this._isReadonly,r=this._isShallow;if(s==="__v_isReactive")return!i;if(s==="__v_isReadonly")return i;if(s==="__v_isShallow")return r;if(s==="__v_raw")return n===(i?r?gr:Jn:r?qn:Gn).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const l=I(t);if(!i){let u;if(l&&(u=ki[s]))return u;if(s==="hasOwnProperty")return sr}const f=Reflect.get(t,s,se(t)?t:n);return(et(s)?Wn.has(s):tr(s))||(i||X(t,"get",s),r)?f:se(f)?l&&Cs(s)?f:f.value:z(f)?i?Yn(f):Ds(f):f}}class zn extends Kn{constructor(t=!1){super(!1,t)}set(t,s,n,i){let r=t[s];if(!this._isShallow){const u=Qe(r);if(!xe(n)&&!Qe(n)&&(r=j(r),n=j(n)),!I(t)&&se(r)&&!se(n))return u?!1:(r.value=n,!0)}const l=I(t)&&Cs(s)?Number(s)e,Kt=e=>Reflect.getPrototypeOf(e);function Mt(e,t,s=!1,n=!1){e=e.__v_raw;const i=j(e),r=j(t);s||(ze(t,r)&&X(i,"get",t),X(i,"get",r));const{has:l}=Kt(i),f=n?Fs:s?js:ce;if(l.call(i,t))return f(e.get(t));if(l.call(i,r))return f(e.get(r));e!==i&&e.get(t)}function Pt(e,t=!1){const s=this.__v_raw,n=j(s),i=j(e);return t||(ze(e,i)&&X(n,"has",e),X(n,"has",i)),e===i?s.has(e):s.has(e)||s.has(i)}function It(e,t=!1){return e=e.__v_raw,!t&&X(j(e),"iterate",We),Reflect.get(e,"size",e)}function sn(e,t=!1){!t&&!xe(e)&&!Qe(e)&&(e=j(e));const s=j(this);return Kt(s).has.call(s,e)||(s.add(e),Te(s,"add",e,e)),this}function nn(e,t,s=!1){!s&&!xe(t)&&!Qe(t)&&(t=j(t));const n=j(this),{has:i,get:r}=Kt(n);let l=i.call(n,e);l||(e=j(e),l=i.call(n,e));const f=r.call(n,e);return n.set(e,t),l?ze(t,f)&&Te(n,"set",e,t):Te(n,"add",e,t),this}function rn(e){const t=j(this),{has:s,get:n}=Kt(t);let i=s.call(t,e);i||(e=j(e),i=s.call(t,e)),n&&n.call(t,e);const r=t.delete(e);return i&&Te(t,"delete",e,void 0),r}function ln(){const e=j(this),t=e.size!==0,s=e.clear();return t&&Te(e,"clear",void 0,void 0),s}function At(e,t){return function(n,i){const r=this,l=r.__v_raw,f=j(l),u=t?Fs:e?js:ce;return!e&&X(f,"iterate",We),l.forEach((d,a)=>n.call(i,u(d),u(a),r))}}function Ft(e,t,s){return function(...n){const i=this.__v_raw,r=j(i),l=ct(r),f=e==="entries"||e===Symbol.iterator&&l,u=e==="keys"&&l,d=i[e](...n),a=s?Fs:t?js:ce;return!t&&X(r,"iterate",u?ds:We),{next(){const{value:p,done:y}=d.next();return y?{value:p,done:y}:{value:f?[a(p[0]),a(p[1])]:a(p),done:y}},[Symbol.iterator](){return this}}}}function Ie(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function or(){const e={get(r){return Mt(this,r)},get size(){return It(this)},has:Pt,add:sn,set:nn,delete:rn,clear:ln,forEach:At(!1,!1)},t={get(r){return Mt(this,r,!1,!0)},get size(){return It(this)},has:Pt,add(r){return sn.call(this,r,!0)},set(r,l){return nn.call(this,r,l,!0)},delete:rn,clear:ln,forEach:At(!1,!0)},s={get(r){return Mt(this,r,!0)},get size(){return It(this,!0)},has(r){return Pt.call(this,r,!0)},add:Ie("add"),set:Ie("set"),delete:Ie("delete"),clear:Ie("clear"),forEach:At(!0,!1)},n={get(r){return Mt(this,r,!0,!0)},get size(){return It(this,!0)},has(r){return Pt.call(this,r,!0)},add:Ie("add"),set:Ie("set"),delete:Ie("delete"),clear:Ie("clear"),forEach:At(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(r=>{e[r]=Ft(r,!1,!1),s[r]=Ft(r,!0,!1),t[r]=Ft(r,!1,!0),n[r]=Ft(r,!0,!0)}),[e,s,t,n]}const[cr,fr,ur,ar]=or();function Rs(e,t){const s=t?e?ar:ur:e?fr:cr;return(n,i,r)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(D(s,i)&&i in n?s:n,i,r)}const hr={get:Rs(!1,!1)},dr={get:Rs(!1,!0)},pr={get:Rs(!0,!1)};const Gn=new WeakMap,qn=new WeakMap,Jn=new WeakMap,gr=new WeakMap;function mr(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function _r(e){return e.__v_skip||!Object.isExtensible(e)?0:mr(Ni(e))}function Ds(e){return Qe(e)?e:Hs(e,!1,ir,hr,Gn)}function br(e){return Hs(e,!1,lr,dr,qn)}function Yn(e){return Hs(e,!0,rr,pr,Jn)}function Hs(e,t,s,n,i){if(!z(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const r=i.get(e);if(r)return r;const l=_r(e);if(l===0)return e;const f=new Proxy(e,l===2?n:s);return i.set(e,f),f}function at(e){return Qe(e)?at(e.__v_raw):!!(e&&e.__v_isReactive)}function Qe(e){return!!(e&&e.__v_isReadonly)}function xe(e){return!!(e&&e.__v_isShallow)}function Ls(e){return e?!!e.__v_raw:!1}function j(e){const t=e&&e.__v_raw;return t?j(t):e}function vr(e){return!D(e,"__v_skip")&&Object.isExtensible(e)&&Fn(e,"__v_skip",!0),e}const ce=e=>z(e)?Ds(e):e,js=e=>z(e)?Yn(e):e;function se(e){return e?e.__v_isRef===!0:!1}function xr(e){return se(e)?e.value:e}const yr={get:(e,t,s)=>t==="__v_raw"?e:xr(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const i=e[t];return se(i)&&!se(s)?(i.value=s,!0):Reflect.set(e,t,s,n)}};function Xn(e){return at(e)?e:new Proxy(e,yr)}class wr{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new Vn(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=gt-1,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){this.flags|=16,N!==this&&this.dep.notify()}get value(){const t=this.dep.track();return $n(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Er(e,t,s=!1){let n,i;return P(e)?n=e:(n=e.get,i=e.set),new wr(n,i,s)}const Rt={},jt=new WeakMap;let Ue;function Sr(e,t=!1,s=Ue){if(s){let n=jt.get(s);n||jt.set(s,n=[]),n.push(e)}}function Cr(e,t,s=V){const{immediate:n,deep:i,once:r,scheduler:l,augmentJob:f,call:u}=s,d=T=>i?T:xe(T)||i===!1||i===0?Re(T,1):Re(T);let a,p,y,M,R=!1,F=!1;if(se(e)?(p=()=>e.value,R=xe(e)):at(e)?(p=()=>d(e),R=!0):I(e)?(F=!0,R=e.some(T=>at(T)||xe(T)),p=()=>e.map(T=>{if(se(T))return T.value;if(at(T))return d(T);if(P(T))return u?u(T,2):T()})):P(e)?t?p=u?()=>u(e,2):e:p=()=>{if(y){He();try{y()}finally{Le()}}const T=Ue;Ue=a;try{return u?u(e,3,[M]):e(M)}finally{Ue=T}}:p=ve,t&&i){const T=p,G=i===!0?1/0:i;p=()=>Re(T(),G)}const Z=Xi(),L=()=>{a.stop(),Z&&Ss(Z.effects,a)};if(r)if(t){const T=t;t=(...G)=>{T(...G),L()}}else{const T=p;p=()=>{T(),L()}}let W=F?new Array(e.length).fill(Rt):Rt;const K=T=>{if(!(!(a.flags&1)||!a.dirty&&!T))if(t){const G=a.run();if(i||R||(F?G.some((Me,he)=>ze(Me,W[he])):ze(G,W))){y&&y();const Me=Ue;Ue=a;try{const he=[G,W===Rt?void 0:F&&W[0]===Rt?[]:W,M];u?u(t,3,he):t(...he),W=G}finally{Ue=Me}}}else a.run()};return f&&f(K),a=new Hn(p),a.scheduler=l?()=>l(K,!1):K,M=T=>Sr(T,!1,a),y=a.onStop=()=>{const T=jt.get(a);if(T){if(u)u(T,4);else for(const G of T)G();jt.delete(a)}},t?n?K(!0):W=a.run():l?l(K.bind(null,!0),!0):a.run(),L.pause=a.pause.bind(a),L.resume=a.resume.bind(a),L.stop=L,L}function Re(e,t=1/0,s){if(t<=0||!z(e)||e.__v_skip||(s=s||new Set,s.has(e)))return e;if(s.add(e),t--,se(e))Re(e.value,t,s);else if(I(e))for(let n=0;n{Re(n,t,s)});else if($i(e)){for(const n in e)Re(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&Re(e[n],t,s)}return e}/**
10 | * @vue/runtime-core v3.5.5
11 | * (c) 2018-present Yuxi (Evan) You and Vue contributors
12 | * @license MIT
13 | **/function yt(e,t,s,n){try{return n?e(...n):e()}catch(i){zt(i,t,s)}}function ye(e,t,s,n){if(P(e)){const i=yt(e,t,s,n);return i&&In(i)&&i.catch(r=>{zt(r,t,s)}),i}if(I(e)){const i=[];for(let r=0;r>>1,i=ee[n],r=bt(i);r=bt(s)?ee.push(e):ee.splice(Mr(t),0,e),e.flags|=1,Qn()}}function Qn(){!_t&&!ps&&(ps=!0,Ns=Zn.then(ei))}function Pr(e){I(e)?Xe.push(...e):Ae&&e.id===-1?Ae.splice(Je+1,0,e):e.flags&1||(Xe.push(e),e.flags|=1),Qn()}function on(e,t,s=_t?me+1:0){for(;sbt(s)-bt(n));if(Xe.length=0,Ae){Ae.push(...t);return}for(Ae=t,Je=0;Jee.id==null?e.flags&2?-1:1/0:e.id;function ei(e){ps=!1,_t=!0;try{for(me=0;me{n._d&&gn(-1);const r=Nt(t);let l;try{l=e(...i)}finally{Nt(r),n._d&&gn(1)}return l};return n._n=!0,n._c=!0,n._d=!0,n}function Be(e,t,s,n){const i=e.dirs,r=t&&t.dirs;for(let l=0;le.__isTeleport;function Bs(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Bs(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}/*! #__NO_SIDE_EFFECTS__ */function si(e,t){return P(e)?J({name:e.name},t,{setup:e}):e}function ni(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function gs(e,t,s,n,i=!1){if(I(e)){e.forEach((R,F)=>gs(R,t&&(I(t)?t[F]:t),s,n,i));return}if(ht(n)&&!i)return;const r=n.shapeFlag&4?Ks(n.component):n.el,l=i?null:r,{i:f,r:u}=e,d=t&&t.r,a=f.refs===V?f.refs={}:f.refs,p=f.setupState,y=j(p),M=p===V?()=>!1:R=>D(y,R);if(d!=null&&d!==u&&(q(d)?(a[d]=null,M(d)&&(p[d]=null)):se(d)&&(d.value=null)),P(u))yt(u,f,12,[l,a]);else{const R=q(u),F=se(u);if(R||F){const Z=()=>{if(e.f){const L=R?M(u)?p[u]:a[u]:u.value;i?I(L)&&Ss(L,r):I(L)?L.includes(r)||L.push(r):R?(a[u]=[r],M(u)&&(p[u]=a[u])):(u.value=[r],e.k&&(a[e.k]=u.value))}else R?(a[u]=l,M(u)&&(p[u]=l)):F&&(u.value=l,e.k&&(a[e.k]=l))};l?(Z.id=-1,le(Z,s)):Z()}}}const ht=e=>!!e.type.__asyncLoader,ii=e=>e.type.__isKeepAlive;function Rr(e,t){ri(e,"a",t)}function Dr(e,t){ri(e,"da",t)}function ri(e,t,s=te){const n=e.__wdc||(e.__wdc=()=>{let i=s;for(;i;){if(i.isDeactivated)return;i=i.parent}return e()});if(Gt(t,n,s),s){let i=s.parent;for(;i&&i.parent;)ii(i.parent.vnode)&&Hr(n,t,s,i),i=i.parent}}function Hr(e,t,s,n){const i=Gt(t,e,n,!0);oi(()=>{Ss(n[t],i)},s)}function Gt(e,t,s=te,n=!1){if(s){const i=s[e]||(s[e]=[]),r=t.__weh||(t.__weh=(...l)=>{He();const f=wt(s),u=ye(t,s,e,l);return f(),Le(),u});return n?i.unshift(r):i.push(r),r}}const Oe=e=>(t,s=te)=>{(!Yt||e==="sp")&&Gt(e,(...n)=>t(...n),s)},Lr=Oe("bm"),li=Oe("m"),jr=Oe("bu"),Nr=Oe("u"),$r=Oe("bum"),oi=Oe("um"),Br=Oe("sp"),Vr=Oe("rtg"),Ur=Oe("rtc");function Wr(e,t=te){Gt("ec",e,t)}const Kr=Symbol.for("v-ndc"),ms=e=>e?Pi(e)?Ks(e):ms(e.parent):null,dt=J(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>ms(e.parent),$root:e=>ms(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Vs(e),$forceUpdate:e=>e.f||(e.f=()=>{$s(e.update)}),$nextTick:e=>e.n||(e.n=Or.bind(e.proxy)),$watch:e=>hl.bind(e)}),is=(e,t)=>e!==V&&!e.__isScriptSetup&&D(e,t),zr={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:i,props:r,accessCache:l,type:f,appContext:u}=e;let d;if(t[0]!=="$"){const M=l[t];if(M!==void 0)switch(M){case 1:return n[t];case 2:return i[t];case 4:return s[t];case 3:return r[t]}else{if(is(n,t))return l[t]=1,n[t];if(i!==V&&D(i,t))return l[t]=2,i[t];if((d=e.propsOptions[0])&&D(d,t))return l[t]=3,r[t];if(s!==V&&D(s,t))return l[t]=4,s[t];_s&&(l[t]=0)}}const a=dt[t];let p,y;if(a)return t==="$attrs"&&X(e.attrs,"get",""),a(e);if((p=f.__cssModules)&&(p=p[t]))return p;if(s!==V&&D(s,t))return l[t]=4,s[t];if(y=u.config.globalProperties,D(y,t))return y[t]},set({_:e},t,s){const{data:n,setupState:i,ctx:r}=e;return is(i,t)?(i[t]=s,!0):n!==V&&D(n,t)?(n[t]=s,!0):D(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(r[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:i,propsOptions:r}},l){let f;return!!s[l]||e!==V&&D(e,l)||is(t,l)||(f=r[0])&&D(f,l)||D(n,l)||D(dt,l)||D(i.config.globalProperties,l)},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:D(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}};function cn(e){return I(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}let _s=!0;function Gr(e){const t=Vs(e),s=e.proxy,n=e.ctx;_s=!1,t.beforeCreate&&fn(t.beforeCreate,e,"bc");const{data:i,computed:r,methods:l,watch:f,provide:u,inject:d,created:a,beforeMount:p,mounted:y,beforeUpdate:M,updated:R,activated:F,deactivated:Z,beforeDestroy:L,beforeUnmount:W,destroyed:K,unmounted:T,render:G,renderTracked:Me,renderTriggered:he,errorCaptured:Pe,serverPrefetch:Et,expose:je,inheritAttrs:tt,components:St,directives:Ct,filters:Zt}=t;if(d&&qr(d,n,null),l)for(const U in l){const $=l[U];P($)&&(n[U]=$.bind(s))}if(i){const U=i.call(s,s);z(U)&&(e.data=Ds(U))}if(_s=!0,r)for(const U in r){const $=r[U],Ne=P($)?$.bind(s,s):P($.get)?$.get.bind(s,s):ve,Tt=!P($)&&P($.set)?$.set.bind(s):ve,$e=Ll({get:Ne,set:Tt});Object.defineProperty(n,U,{enumerable:!0,configurable:!0,get:()=>$e.value,set:de=>$e.value=de})}if(f)for(const U in f)ci(f[U],n,s,U);if(u){const U=P(u)?u.call(s):u;Reflect.ownKeys(U).forEach($=>{kr($,U[$])})}a&&fn(a,e,"c");function Q(U,$){I($)?$.forEach(Ne=>U(Ne.bind(s))):$&&U($.bind(s))}if(Q(Lr,p),Q(li,y),Q(jr,M),Q(Nr,R),Q(Rr,F),Q(Dr,Z),Q(Wr,Pe),Q(Ur,Me),Q(Vr,he),Q($r,W),Q(oi,T),Q(Br,Et),I(je))if(je.length){const U=e.exposed||(e.exposed={});je.forEach($=>{Object.defineProperty(U,$,{get:()=>s[$],set:Ne=>s[$]=Ne})})}else e.exposed||(e.exposed={});G&&e.render===ve&&(e.render=G),tt!=null&&(e.inheritAttrs=tt),St&&(e.components=St),Ct&&(e.directives=Ct),Et&&ni(e)}function qr(e,t,s=ve){I(e)&&(e=bs(e));for(const n in e){const i=e[n];let r;z(i)?"default"in i?r=Dt(i.from||n,i.default,!0):r=Dt(i.from||n):r=Dt(i),se(r)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>r.value,set:l=>r.value=l}):t[n]=r}}function fn(e,t,s){ye(I(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function ci(e,t,s,n){let i=n.includes(".")?wi(s,n):()=>s[n];if(q(e)){const r=t[e];P(r)&&ls(i,r)}else if(P(e))ls(i,e.bind(s));else if(z(e))if(I(e))e.forEach(r=>ci(r,t,s,n));else{const r=P(e.handler)?e.handler.bind(s):t[e.handler];P(r)&&ls(i,r,e)}}function Vs(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:i,optionsCache:r,config:{optionMergeStrategies:l}}=e.appContext,f=r.get(t);let u;return f?u=f:!i.length&&!s&&!n?u=t:(u={},i.length&&i.forEach(d=>$t(u,d,l,!0)),$t(u,t,l)),z(t)&&r.set(t,u),u}function $t(e,t,s,n=!1){const{mixins:i,extends:r}=t;r&&$t(e,r,s,!0),i&&i.forEach(l=>$t(e,l,s,!0));for(const l in t)if(!(n&&l==="expose")){const f=Jr[l]||s&&s[l];e[l]=f?f(e[l],t[l]):t[l]}return e}const Jr={data:un,props:an,emits:an,methods:ot,computed:ot,beforeCreate:k,created:k,beforeMount:k,mounted:k,beforeUpdate:k,updated:k,beforeDestroy:k,beforeUnmount:k,destroyed:k,unmounted:k,activated:k,deactivated:k,errorCaptured:k,serverPrefetch:k,components:ot,directives:ot,watch:Xr,provide:un,inject:Yr};function un(e,t){return t?e?function(){return J(P(e)?e.call(this,this):e,P(t)?t.call(this,this):t)}:t:e}function Yr(e,t){return ot(bs(e),bs(t))}function bs(e){if(I(e)){const t={};for(let s=0;s1)return s&&P(t)?t.call(n&&n.proxy):t}}const ui={},ai=()=>Object.create(ui),hi=e=>Object.getPrototypeOf(e)===ui;function el(e,t,s,n=!1){const i={},r=ai();e.propsDefaults=Object.create(null),di(e,t,i,r);for(const l in e.propsOptions[0])l in i||(i[l]=void 0);s?e.props=n?i:br(i):e.type.props?e.props=i:e.props=r,e.attrs=r}function tl(e,t,s,n){const{props:i,attrs:r,vnode:{patchFlag:l}}=e,f=j(i),[u]=e.propsOptions;let d=!1;if((n||l>0)&&!(l&16)){if(l&8){const a=e.vnode.dynamicProps;for(let p=0;p{u=!0;const[y,M]=pi(p,t,!0);J(l,y),M&&f.push(...M)};!s&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!r&&!u)return z(e)&&n.set(e,Ye),Ye;if(I(r))for(let a=0;ae[0]==="_"||e==="$stable",Us=e=>I(e)?e.map(_e):[_e(e)],nl=(e,t,s)=>{if(t._n)return t;const n=Ir((...i)=>Us(t(...i)),s);return n._c=!1,n},mi=(e,t,s)=>{const n=e._ctx;for(const i in e){if(gi(i))continue;const r=e[i];if(P(r))t[i]=nl(i,r,n);else if(r!=null){const l=Us(r);t[i]=()=>l}}},_i=(e,t)=>{const s=Us(t);e.slots.default=()=>s},bi=(e,t,s)=>{for(const n in t)(s||n!=="_")&&(e[n]=t[n])},il=(e,t,s)=>{const n=e.slots=ai();if(e.vnode.shapeFlag&32){const i=t._;i?(bi(n,t,s),s&&Fn(n,"_",i,!0)):mi(t,n)}else t&&_i(e,t)},rl=(e,t,s)=>{const{vnode:n,slots:i}=e;let r=!0,l=V;if(n.shapeFlag&32){const f=t._;f?s&&f===1?r=!1:bi(i,t,s):(r=!t.$stable,mi(t,i)),l=t}else t&&(_i(e,t),l={default:1});if(r)for(const f in i)!gi(f)&&l[f]==null&&delete i[f]},le=vl;function ll(e){return ol(e)}function ol(e,t){const s=Rn();s.__VUE__=!0;const{insert:n,remove:i,patchProp:r,createElement:l,createText:f,createComment:u,setText:d,setElementText:a,parentNode:p,nextSibling:y,setScopeId:M=ve,insertStaticContent:R}=e,F=(o,c,h,_=null,g=null,m=null,w=void 0,x=null,v=!!c.dynamicChildren)=>{if(o===c)return;o&&!lt(o,c)&&(_=Ot(o),de(o,g,m,!0),o=null),c.patchFlag===-2&&(v=!1,c.dynamicChildren=null);const{type:b,ref:C,shapeFlag:E}=c;switch(b){case Jt:Z(o,c,h,_);break;case vt:L(o,c,h,_);break;case cs:o==null&&W(c,h,_,w);break;case Ce:St(o,c,h,_,g,m,w,x,v);break;default:E&1?G(o,c,h,_,g,m,w,x,v):E&6?Ct(o,c,h,_,g,m,w,x,v):(E&64||E&128)&&b.process(o,c,h,_,g,m,w,x,v,nt)}C!=null&&g&&gs(C,o&&o.ref,m,c||o,!c)},Z=(o,c,h,_)=>{if(o==null)n(c.el=f(c.children),h,_);else{const g=c.el=o.el;c.children!==o.children&&d(g,c.children)}},L=(o,c,h,_)=>{o==null?n(c.el=u(c.children||""),h,_):c.el=o.el},W=(o,c,h,_)=>{[o.el,o.anchor]=R(o.children,c,h,_,o.el,o.anchor)},K=({el:o,anchor:c},h,_)=>{let g;for(;o&&o!==c;)g=y(o),n(o,h,_),o=g;n(c,h,_)},T=({el:o,anchor:c})=>{let h;for(;o&&o!==c;)h=y(o),i(o),o=h;i(c)},G=(o,c,h,_,g,m,w,x,v)=>{c.type==="svg"?w="svg":c.type==="math"&&(w="mathml"),o==null?Me(c,h,_,g,m,w,x,v):Et(o,c,g,m,w,x,v)},Me=(o,c,h,_,g,m,w,x)=>{let v,b;const{props:C,shapeFlag:E,transition:S,dirs:O}=o;if(v=o.el=l(o.type,m,C&&C.is,C),E&8?a(v,o.children):E&16&&Pe(o.children,v,null,_,g,rs(o,m),w,x),O&&Be(o,null,_,"created"),he(v,o,o.scopeId,w,_),C){for(const B in C)B!=="value"&&!ft(B)&&r(v,B,null,C[B],m,_);"value"in C&&r(v,"value",null,C.value,m),(b=C.onVnodeBeforeMount)&&ge(b,_,o)}O&&Be(o,null,_,"beforeMount");const A=cl(g,S);A&&S.beforeEnter(v),n(v,c,h),((b=C&&C.onVnodeMounted)||A||O)&&le(()=>{b&&ge(b,_,o),A&&S.enter(v),O&&Be(o,null,_,"mounted")},g)},he=(o,c,h,_,g)=>{if(h&&M(o,h),_)for(let m=0;m<_.length;m++)M(o,_[m]);if(g){let m=g.subTree;if(c===m||Si(m.type)&&(m.ssContent===c||m.ssFallback===c)){const w=g.vnode;he(o,w,w.scopeId,w.slotScopeIds,g.parent)}}},Pe=(o,c,h,_,g,m,w,x,v=0)=>{for(let b=v;b{const x=c.el=o.el;let{patchFlag:v,dynamicChildren:b,dirs:C}=c;v|=o.patchFlag&16;const E=o.props||V,S=c.props||V;let O;if(h&&Ve(h,!1),(O=S.onVnodeBeforeUpdate)&&ge(O,h,c,o),C&&Be(c,o,h,"beforeUpdate"),h&&Ve(h,!0),(E.innerHTML&&S.innerHTML==null||E.textContent&&S.textContent==null)&&a(x,""),b?je(o.dynamicChildren,b,x,h,_,rs(c,g),m):w||$(o,c,x,null,h,_,rs(c,g),m,!1),v>0){if(v&16)tt(x,E,S,h,g);else if(v&2&&E.class!==S.class&&r(x,"class",null,S.class,g),v&4&&r(x,"style",E.style,S.style,g),v&8){const A=c.dynamicProps;for(let B=0;B{O&&ge(O,h,c,o),C&&Be(c,o,h,"updated")},_)},je=(o,c,h,_,g,m,w)=>{for(let x=0;x{if(c!==h){if(c!==V)for(const m in c)!ft(m)&&!(m in h)&&r(o,m,c[m],null,g,_);for(const m in h){if(ft(m))continue;const w=h[m],x=c[m];w!==x&&m!=="value"&&r(o,m,x,w,g,_)}"value"in h&&r(o,"value",c.value,h.value,g)}},St=(o,c,h,_,g,m,w,x,v)=>{const b=c.el=o?o.el:f(""),C=c.anchor=o?o.anchor:f("");let{patchFlag:E,dynamicChildren:S,slotScopeIds:O}=c;O&&(x=x?x.concat(O):O),o==null?(n(b,h,_),n(C,h,_),Pe(c.children||[],h,C,g,m,w,x,v)):E>0&&E&64&&S&&o.dynamicChildren?(je(o.dynamicChildren,S,h,g,m,w,x),(c.key!=null||g&&c===g.subTree)&&vi(o,c,!0)):$(o,c,h,C,g,m,w,x,v)},Ct=(o,c,h,_,g,m,w,x,v)=>{c.slotScopeIds=x,o==null?c.shapeFlag&512?g.ctx.activate(c,h,_,w,v):Zt(c,h,_,g,m,w,v):Gs(o,c,v)},Zt=(o,c,h,_,g,m,w)=>{const x=o.component=Il(o,_,g);if(ii(o)&&(x.ctx.renderer=nt),Al(x,!1,w),x.asyncDep){if(g&&g.registerDep(x,Q,w),!o.el){const v=x.subTree=De(vt);L(null,v,c,h)}}else Q(x,o,c,h,g,m,w)},Gs=(o,c,h)=>{const _=c.component=o.component;if(_l(o,c,h))if(_.asyncDep&&!_.asyncResolved){U(_,c,h);return}else _.next=c,_.update();else c.el=o.el,_.vnode=c},Q=(o,c,h,_,g,m,w)=>{const x=()=>{if(o.isMounted){let{next:E,bu:S,u:O,parent:A,vnode:B}=o;{const ie=xi(o);if(ie){E&&(E.el=B.el,U(o,E,w)),ie.asyncDep.then(()=>{o.isUnmounted||x()});return}}let H=E,ne;Ve(o,!1),E?(E.el=B.el,U(o,E,w)):E=B,S&&es(S),(ne=E.props&&E.props.onVnodeBeforeUpdate)&&ge(ne,A,E,B),Ve(o,!0);const Y=os(o),ue=o.subTree;o.subTree=Y,F(ue,Y,p(ue.el),Ot(ue),o,g,m),E.el=Y.el,H===null&&bl(o,Y.el),O&&le(O,g),(ne=E.props&&E.props.onVnodeUpdated)&&le(()=>ge(ne,A,E,B),g)}else{let E;const{el:S,props:O}=c,{bm:A,m:B,parent:H,root:ne,type:Y}=o,ue=ht(c);if(Ve(o,!1),A&&es(A),!ue&&(E=O&&O.onVnodeBeforeMount)&&ge(E,H,c),Ve(o,!0),S&&Xs){const ie=()=>{o.subTree=os(o),Xs(S,o.subTree,o,g,null)};ue&&Y.__asyncHydrate?Y.__asyncHydrate(S,o,ie):ie()}else{ne.ce&&ne.ce._injectChildStyle(Y);const ie=o.subTree=os(o);F(null,ie,h,_,o,g,m),c.el=ie.el}if(B&&le(B,g),!ue&&(E=O&&O.onVnodeMounted)){const ie=c;le(()=>ge(E,H,ie),g)}(c.shapeFlag&256||H&&ht(H.vnode)&&H.vnode.shapeFlag&256)&&o.a&&le(o.a,g),o.isMounted=!0,c=h=_=null}};o.scope.on();const v=o.effect=new Hn(x);o.scope.off();const b=o.update=v.run.bind(v),C=o.job=v.runIfDirty.bind(v);C.i=o,C.id=o.uid,v.scheduler=()=>$s(C),Ve(o,!0),b()},U=(o,c,h)=>{c.component=o;const _=o.vnode.props;o.vnode=c,o.next=null,tl(o,c.props,_,h),rl(o,c.children,h),He(),on(o),Le()},$=(o,c,h,_,g,m,w,x,v=!1)=>{const b=o&&o.children,C=o?o.shapeFlag:0,E=c.children,{patchFlag:S,shapeFlag:O}=c;if(S>0){if(S&128){Tt(b,E,h,_,g,m,w,x,v);return}else if(S&256){Ne(b,E,h,_,g,m,w,x,v);return}}O&8?(C&16&&st(b,g,m),E!==b&&a(h,E)):C&16?O&16?Tt(b,E,h,_,g,m,w,x,v):st(b,g,m,!0):(C&8&&a(h,""),O&16&&Pe(E,h,_,g,m,w,x,v))},Ne=(o,c,h,_,g,m,w,x,v)=>{o=o||Ye,c=c||Ye;const b=o.length,C=c.length,E=Math.min(b,C);let S;for(S=0;SC?st(o,g,m,!0,!1,E):Pe(c,h,_,g,m,w,x,v,E)},Tt=(o,c,h,_,g,m,w,x,v)=>{let b=0;const C=c.length;let E=o.length-1,S=C-1;for(;b<=E&&b<=S;){const O=o[b],A=c[b]=v?Fe(c[b]):_e(c[b]);if(lt(O,A))F(O,A,h,null,g,m,w,x,v);else break;b++}for(;b<=E&&b<=S;){const O=o[E],A=c[S]=v?Fe(c[S]):_e(c[S]);if(lt(O,A))F(O,A,h,null,g,m,w,x,v);else break;E--,S--}if(b>E){if(b<=S){const O=S+1,A=OS)for(;b<=E;)de(o[b],g,m,!0),b++;else{const O=b,A=b,B=new Map;for(b=A;b<=S;b++){const re=c[b]=v?Fe(c[b]):_e(c[b]);re.key!=null&&B.set(re.key,b)}let H,ne=0;const Y=S-A+1;let ue=!1,ie=0;const it=new Array(Y);for(b=0;b=Y){de(re,g,m,!0);continue}let pe;if(re.key!=null)pe=B.get(re.key);else for(H=A;H<=S;H++)if(it[H-A]===0&<(re,c[H])){pe=H;break}pe===void 0?de(re,g,m,!0):(it[pe-A]=b+1,pe>=ie?ie=pe:ue=!0,F(re,c[pe],h,null,g,m,w,x,v),ne++)}const Zs=ue?fl(it):Ye;for(H=Zs.length-1,b=Y-1;b>=0;b--){const re=A+b,pe=c[re],Qs=re+1{const{el:m,type:w,transition:x,children:v,shapeFlag:b}=o;if(b&6){$e(o.component.subTree,c,h,_);return}if(b&128){o.suspense.move(c,h,_);return}if(b&64){w.move(o,c,h,nt);return}if(w===Ce){n(m,c,h);for(let E=0;Ex.enter(m),g);else{const{leave:E,delayLeave:S,afterLeave:O}=x,A=()=>n(m,c,h),B=()=>{E(m,()=>{A(),O&&O()})};S?S(m,A,B):B()}else n(m,c,h)},de=(o,c,h,_=!1,g=!1)=>{const{type:m,props:w,ref:x,children:v,dynamicChildren:b,shapeFlag:C,patchFlag:E,dirs:S,cacheIndex:O}=o;if(E===-2&&(g=!1),x!=null&&gs(x,null,h,o,!0),O!=null&&(c.renderCache[O]=void 0),C&256){c.ctx.deactivate(o);return}const A=C&1&&S,B=!ht(o);let H;if(B&&(H=w&&w.onVnodeBeforeUnmount)&&ge(H,c,o),C&6)Ri(o.component,h,_);else{if(C&128){o.suspense.unmount(h,_);return}A&&Be(o,null,c,"beforeUnmount"),C&64?o.type.remove(o,c,h,nt,_):b&&!b.hasOnce&&(m!==Ce||E>0&&E&64)?st(b,c,h,!1,!0):(m===Ce&&E&384||!g&&C&16)&&st(v,c,h),_&&qs(o)}(B&&(H=w&&w.onVnodeUnmounted)||A)&&le(()=>{H&&ge(H,c,o),A&&Be(o,null,c,"unmounted")},h)},qs=o=>{const{type:c,el:h,anchor:_,transition:g}=o;if(c===Ce){Fi(h,_);return}if(c===cs){T(o);return}const m=()=>{i(h),g&&!g.persisted&&g.afterLeave&&g.afterLeave()};if(o.shapeFlag&1&&g&&!g.persisted){const{leave:w,delayLeave:x}=g,v=()=>w(h,m);x?x(o.el,m,v):v()}else m()},Fi=(o,c)=>{let h;for(;o!==c;)h=y(o),i(o),o=h;i(c)},Ri=(o,c,h)=>{const{bum:_,scope:g,job:m,subTree:w,um:x,m:v,a:b}=o;dn(v),dn(b),_&&es(_),g.stop(),m&&(m.flags|=8,de(w,o,c,h)),x&&le(x,c),le(()=>{o.isUnmounted=!0},c),c&&c.pendingBranch&&!c.isUnmounted&&o.asyncDep&&!o.asyncResolved&&o.suspenseId===c.pendingId&&(c.deps--,c.deps===0&&c.resolve())},st=(o,c,h,_=!1,g=!1,m=0)=>{for(let w=m;w{if(o.shapeFlag&6)return Ot(o.component.subTree);if(o.shapeFlag&128)return o.suspense.next();const c=y(o.anchor||o.el),h=c&&c[Ar];return h?y(h):c};let Qt=!1;const Js=(o,c,h)=>{o==null?c._vnode&&de(c._vnode,null,null,!0):F(c._vnode||null,o,c,null,null,null,h),c._vnode=o,Qt||(Qt=!0,on(),kn(),Qt=!1)},nt={p:F,um:de,m:$e,r:qs,mt:Zt,mc:Pe,pc:$,pbc:je,n:Ot,o:e};let Ys,Xs;return{render:Js,hydrate:Ys,createApp:Qr(Js,Ys)}}function rs({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function Ve({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function cl(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function vi(e,t,s=!1){const n=e.children,i=t.children;if(I(n)&&I(i))for(let r=0;r>1,e[s[f]]0&&(t[n]=s[r-1]),s[r]=n)}}for(r=s.length,l=s[r-1];r-- >0;)s[r]=l,l=t[l];return s}function xi(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:xi(t)}function dn(e){if(e)for(let t=0;tDt(ul);function ls(e,t,s){return yi(e,t,s)}function yi(e,t,s=V){const{immediate:n,deep:i,flush:r,once:l}=s,f=J({},s);let u;if(Yt)if(r==="sync"){const y=al();u=y.__watcherHandles||(y.__watcherHandles=[])}else if(!t||n)f.once=!0;else return{stop:ve,resume:ve,pause:ve};const d=te;f.call=(y,M,R)=>ye(y,d,M,R);let a=!1;r==="post"?f.scheduler=y=>{le(y,d&&d.suspense)}:r!=="sync"&&(a=!0,f.scheduler=(y,M)=>{M?y():$s(y)}),f.augmentJob=y=>{t&&(y.flags|=4),a&&(y.flags|=2,d&&(y.id=d.uid,y.i=d))};const p=Cr(e,t,f);return u&&u.push(p),p}function hl(e,t,s){const n=this.proxy,i=q(e)?e.includes(".")?wi(n,e):()=>n[e]:e.bind(n,n);let r;P(t)?r=t:(r=t.handler,s=t);const l=wt(this),f=yi(i,r.bind(n),s);return l(),f}function wi(e,t){const s=t.split(".");return()=>{let n=e;for(let i=0;it==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Ke(t)}Modifiers`]||e[`${Ge(t)}Modifiers`];function pl(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||V;let i=s;const r=t.startsWith("update:"),l=r&&dl(n,t.slice(7));l&&(l.trim&&(i=s.map(a=>q(a)?a.trim():a)),l.number&&(i=s.map(Ui)));let f,u=n[f=kt(t)]||n[f=kt(Ke(t))];!u&&r&&(u=n[f=kt(Ge(t))]),u&&ye(u,e,6,i);const d=n[f+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[f])return;e.emitted[f]=!0,ye(d,e,6,i)}}function Ei(e,t,s=!1){const n=t.emitsCache,i=n.get(e);if(i!==void 0)return i;const r=e.emits;let l={},f=!1;if(!P(e)){const u=d=>{const a=Ei(d,t,!0);a&&(f=!0,J(l,a))};!s&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!r&&!f?(z(e)&&n.set(e,null),null):(I(r)?r.forEach(u=>l[u]=null):J(l,r),z(e)&&n.set(e,l),l)}function qt(e,t){return!e||!Vt(t)?!1:(t=t.slice(2).replace(/Once$/,""),D(e,t[0].toLowerCase()+t.slice(1))||D(e,Ge(t))||D(e,t))}function os(e){const{type:t,vnode:s,proxy:n,withProxy:i,propsOptions:[r],slots:l,attrs:f,emit:u,render:d,renderCache:a,props:p,data:y,setupState:M,ctx:R,inheritAttrs:F}=e,Z=Nt(e);let L,W;try{if(s.shapeFlag&4){const T=i||n,G=T;L=_e(d.call(G,T,a,p,M,y,R)),W=f}else{const T=t;L=_e(T.length>1?T(p,{attrs:f,slots:l,emit:u}):T(p,null)),W=t.props?f:gl(f)}}catch(T){pt.length=0,zt(T,e,1),L=De(vt)}let K=L;if(W&&F!==!1){const T=Object.keys(W),{shapeFlag:G}=K;T.length&&G&7&&(r&&T.some(Es)&&(W=ml(W,r)),K=ke(K,W,!1,!0))}return s.dirs&&(K=ke(K,null,!1,!0),K.dirs=K.dirs?K.dirs.concat(s.dirs):s.dirs),s.transition&&Bs(K,s.transition),L=K,Nt(Z),L}const gl=e=>{let t;for(const s in e)(s==="class"||s==="style"||Vt(s))&&((t||(t={}))[s]=e[s]);return t},ml=(e,t)=>{const s={};for(const n in e)(!Es(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function _l(e,t,s){const{props:n,children:i,component:r}=e,{props:l,children:f,patchFlag:u}=t,d=r.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&u>=0){if(u&1024)return!0;if(u&16)return n?pn(n,l,d):!!l;if(u&8){const a=t.dynamicProps;for(let p=0;pe.__isSuspense;function vl(e,t){t&&t.pendingBranch?I(e)?t.effects.push(...e):t.effects.push(e):Pr(e)}const Ce=Symbol.for("v-fgt"),Jt=Symbol.for("v-txt"),vt=Symbol.for("v-cmt"),cs=Symbol.for("v-stc"),pt=[];let fe=null;function Ci(e=!1){pt.push(fe=e?null:[])}function xl(){pt.pop(),fe=pt[pt.length-1]||null}let xt=1;function gn(e){xt+=e,e<0&&fe&&(fe.hasOnce=!0)}function Ti(e){return e.dynamicChildren=xt>0?fe||Ye:null,xl(),xt>0&&fe&&fe.push(e),e}function yl(e,t,s,n,i,r){return Ti(Mi(e,t,s,n,i,r,!0))}function wl(e,t,s,n,i){return Ti(De(e,t,s,n,i,!0))}function El(e){return e?e.__v_isVNode===!0:!1}function lt(e,t){return e.type===t.type&&e.key===t.key}const Oi=({key:e})=>e??null,Ht=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?q(e)||se(e)||P(e)?{i:be,r:e,k:t,f:!!s}:e:null);function Mi(e,t=null,s=null,n=0,i=null,r=e===Ce?0:1,l=!1,f=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Oi(t),ref:t&&Ht(t),scopeId:ti,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:r,patchFlag:n,dynamicProps:i,dynamicChildren:null,appContext:null,ctx:be};return f?(Ws(u,s),r&128&&e.normalize(u)):s&&(u.shapeFlag|=q(s)?8:16),xt>0&&!l&&fe&&(u.patchFlag>0||r&6)&&u.patchFlag!==32&&fe.push(u),u}const De=Sl;function Sl(e,t=null,s=null,n=0,i=null,r=!1){if((!e||e===Kr)&&(e=vt),El(e)){const f=ke(e,t,!0);return s&&Ws(f,s),xt>0&&!r&&fe&&(f.shapeFlag&6?fe[fe.indexOf(e)]=f:fe.push(f)),f.patchFlag=-2,f}if(Hl(e)&&(e=e.__vccOpts),t){t=Cl(t);let{class:f,style:u}=t;f&&!q(f)&&(t.class=Os(f)),z(u)&&(Ls(u)&&!I(u)&&(u=J({},u)),t.style=Ts(u))}const l=q(e)?1:Si(e)?128:Fr(e)?64:z(e)?4:P(e)?2:0;return Mi(e,t,s,n,i,l,r,!0)}function Cl(e){return e?Ls(e)||hi(e)?J({},e):e:null}function ke(e,t,s=!1,n=!1){const{props:i,ref:r,patchFlag:l,children:f,transition:u}=e,d=t?Ol(i||{},t):i,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&Oi(d),ref:t&&t.ref?s&&r?I(r)?r.concat(Ht(t)):[r,Ht(t)]:Ht(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:f,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==Ce?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:u,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&ke(e.ssContent),ssFallback:e.ssFallback&&ke(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return u&&n&&Bs(a,u.clone(a)),a}function Tl(e=" ",t=0){return De(Jt,null,e,t)}function _e(e){return e==null||typeof e=="boolean"?De(vt):I(e)?De(Ce,null,e.slice()):typeof e=="object"?Fe(e):De(Jt,null,String(e))}function Fe(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:ke(e)}function Ws(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(I(t))s=16;else if(typeof t=="object")if(n&65){const i=t.default;i&&(i._c&&(i._d=!1),Ws(e,i()),i._c&&(i._d=!0));return}else{s=32;const i=t._;!i&&!hi(t)?t._ctx=be:i===3&&be&&(be.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else P(t)?(t={default:t,_ctx:be},s=32):(t=String(t),n&64?(s=16,t=[Tl(t)]):s=8);e.children=t,e.shapeFlag|=s}function Ol(...e){const t={};for(let s=0;s{let i;return(i=e[s])||(i=e[s]=[]),i.push(n),r=>{i.length>1?i.forEach(l=>l(r)):i[0](r)}};Bt=t("__VUE_INSTANCE_SETTERS__",s=>te=s),xs=t("__VUE_SSR_SETTERS__",s=>Yt=s)}const wt=e=>{const t=te;return Bt(e),e.scope.on(),()=>{e.scope.off(),Bt(t)}},mn=()=>{te&&te.scope.off(),Bt(null)};function Pi(e){return e.vnode.shapeFlag&4}let Yt=!1;function Al(e,t=!1,s=!1){t&&xs(t);const{props:n,children:i}=e.vnode,r=Pi(e);el(e,n,r,t),il(e,i,s);const l=r?Fl(e,t):void 0;return t&&xs(!1),l}function Fl(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,zr);const{setup:n}=s;if(n){const i=e.setupContext=n.length>1?Dl(e):null,r=wt(e);He();const l=yt(n,e,0,[e.props,i]);if(Le(),r(),In(l)){if(ht(e)||ni(e),l.then(mn,mn),t)return l.then(f=>{_n(e,f,t)}).catch(f=>{zt(f,e,0)});e.asyncDep=l}else _n(e,l,t)}else Ii(e,t)}function _n(e,t,s){P(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:z(t)&&(e.setupState=Xn(t)),Ii(e,s)}let bn;function Ii(e,t,s){const n=e.type;if(!e.render){if(!t&&bn&&!n.render){const i=n.template||Vs(e).template;if(i){const{isCustomElement:r,compilerOptions:l}=e.appContext.config,{delimiters:f,compilerOptions:u}=n,d=J(J({isCustomElement:r,delimiters:f},l),u);n.render=bn(i,d)}}e.render=n.render||ve}{const i=wt(e);He();try{Gr(e)}finally{Le(),i()}}}const Rl={get(e,t){return X(e,"get",""),e[t]}};function Dl(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,Rl),slots:e.slots,emit:e.emit,expose:t}}function Ks(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Xn(vr(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in dt)return dt[s](e)},has(t,s){return s in t||s in dt}})):e.proxy}function Hl(e){return P(e)&&"__vccOpts"in e}const Ll=(e,t)=>Er(e,t,Yt),jl="3.5.5";/**
14 | * @vue/runtime-dom v3.5.5
15 | * (c) 2018-present Yuxi (Evan) You and Vue contributors
16 | * @license MIT
17 | **/let ys;const vn=typeof window<"u"&&window.trustedTypes;if(vn)try{ys=vn.createPolicy("vue",{createHTML:e=>e})}catch{}const Ai=ys?e=>ys.createHTML(e):e=>e,Nl="http://www.w3.org/2000/svg",$l="http://www.w3.org/1998/Math/MathML",Se=typeof document<"u"?document:null,xn=Se&&Se.createElement("template"),Bl={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const i=t==="svg"?Se.createElementNS(Nl,e):t==="mathml"?Se.createElementNS($l,e):s?Se.createElement(e,{is:s}):Se.createElement(e);return e==="select"&&n&&n.multiple!=null&&i.setAttribute("multiple",n.multiple),i},createText:e=>Se.createTextNode(e),createComment:e=>Se.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Se.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,i,r){const l=s?s.previousSibling:t.lastChild;if(i&&(i===r||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),s),!(i===r||!(i=i.nextSibling)););else{xn.innerHTML=Ai(n==="svg"?`${e} `:n==="mathml"?`${e} `:e);const f=xn.content;if(n==="svg"||n==="mathml"){const u=f.firstChild;for(;u.firstChild;)f.appendChild(u.firstChild);f.removeChild(u)}t.insertBefore(f,s)}return[l?l.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},Vl=Symbol("_vtc");function Ul(e,t,s){const n=e[Vl];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const yn=Symbol("_vod"),Wl=Symbol("_vsh"),Kl=Symbol(""),zl=/(^|;)\s*display\s*:/;function Gl(e,t,s){const n=e.style,i=q(s);let r=!1;if(s&&!i){if(t)if(q(t))for(const l of t.split(";")){const f=l.slice(0,l.indexOf(":")).trim();s[f]==null&&Lt(n,f,"")}else for(const l in t)s[l]==null&&Lt(n,l,"");for(const l in s)l==="display"&&(r=!0),Lt(n,l,s[l])}else if(i){if(t!==s){const l=n[Kl];l&&(s+=";"+l),n.cssText=s,r=zl.test(s)}}else t&&e.removeAttribute("style");yn in e&&(e[yn]=r?n.display:"",e[Wl]&&(n.display="none"))}const wn=/\s*!important$/;function Lt(e,t,s){if(I(s))s.forEach(n=>Lt(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=ql(e,t);wn.test(s)?e.setProperty(Ge(n),s.replace(wn,""),"important"):e[n]=s}}const En=["Webkit","Moz","ms"],fs={};function ql(e,t){const s=fs[t];if(s)return s;let n=Ke(t);if(n!=="filter"&&n in e)return fs[t]=n;n=An(n);for(let i=0;ius||(kl.then(()=>us=0),us=Date.now());function to(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;ye(so(n,s.value),t,5,[n])};return s.value=e,s.attached=eo(),s}function so(e,t){if(I(t)){const s=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{s.call(e),e._stopped=!0},t.map(n=>i=>!i._stopped&&n&&n(i))}else return t}const Mn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,no=(e,t,s,n,i,r)=>{const l=i==="svg";t==="class"?Ul(e,n,l):t==="style"?Gl(e,s,n):Vt(t)?Es(t)||Zl(e,t,s,n,r):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):io(e,t,n,l))?(Jl(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Cn(e,t,n,l,r,t!=="value")):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Cn(e,t,n,l))};function io(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&Mn(t)&&P(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const i=e.tagName;if(i==="IMG"||i==="VIDEO"||i==="CANVAS"||i==="SOURCE")return!1}return Mn(t)&&q(s)?!1:!!(t in e||e._isVueCE&&(/[A-Z]/.test(t)||!q(s)))}const ro=J({patchProp:no},Bl);let Pn;function lo(){return Pn||(Pn=ll(ro))}const oo=(...e)=>{const t=lo().createApp(...e),{mount:s}=t;return t.mount=n=>{const i=fo(n);if(!i)return;const r=t._component;!P(r)&&!r.render&&!r.template&&(r.template=i.innerHTML),i.nodeType===1&&(i.textContent="");const l=s(i,!1,co(i));return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),l},t};function co(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function fo(e){return q(e)?document.querySelector(e):e}const Ee=(e=1,t=e+1,s=!1)=>{const n=parseFloat(e),i=parseFloat(t),r=Math.random()*(i-n)+n;return s?Math.round(r):r};class Xt{constructor({color:t="blue",size:s=10,dropRate:n=10}={}){this.color=t,this.size=s,this.dropRate=n}setup({canvas:t,wind:s,windPosCoef:n,windSpeedMax:i,count:r}){return this.canvas=t,this.wind=s,this.windPosCoef=n,this.windSpeedMax=i,this.x=Ee(-35,this.canvas.width+35),this.y=Ee(-30,-35),this.d=Ee(150)+10,this.particleSize=Ee(this.size,this.size*2),this.tilt=Ee(10),this.tiltAngleIncremental=(Ee(0,.08)+.04)*(Ee()<.5?-1:1),this.tiltAngle=0,this.angle=Ee(Math.PI*2),this.count=r+1,this.remove=!1,this}update(){this.tiltAngle+=this.tiltAngleIncremental*(Math.cos(this.wind+(this.d+this.x+this.y)*this.windPosCoef)*.2+1),this.y+=(Math.cos(this.angle+this.d)+parseInt(this.dropRate,10))/2,this.x+=(Math.sin(this.angle)+Math.cos(this.wind+(this.d+this.x+this.y)*this.windPosCoef))*this.windSpeedMax,this.y+=Math.sin(this.wind+(this.d+this.x+this.y)*this.windPosCoef)*this.windSpeedMax,this.tilt=Math.sin(this.tiltAngle-this.count/3)*15}pastBottom(){return this.y>this.canvas.height}draw(){this.canvas.ctx.fillStyle=this.color,this.canvas.ctx.beginPath(),this.canvas.ctx.setTransform(Math.cos(this.tiltAngle),Math.sin(this.tiltAngle),0,1,this.x,this.y)}kill(){this.remove=!0}}class uo extends Xt{draw(){super.draw(),this.canvas.ctx.arc(0,0,this.particleSize/2,0,Math.PI*2,!1),this.canvas.ctx.fill()}}class ao extends Xt{draw(){super.draw(),this.canvas.ctx.fillRect(0,0,this.particleSize,this.particleSize/2)}}class ho extends Xt{draw(){super.draw();const t=(s,n,i,r,l,f)=>{this.canvas.ctx.bezierCurveTo(s*(this.particleSize/200),n*(this.particleSize/200),i*(this.particleSize/200),r*(this.particleSize/200),l*(this.particleSize/200),f*(this.particleSize/200))};this.canvas.ctx.moveTo(37.5/this.particleSize,20/this.particleSize),t(75,37,70,25,50,25),t(20,25,20,62.5,20,62.5),t(20,80,40,102,75,120),t(110,102,130,80,130,62.5),t(130,62.5,130,25,100,25),t(85,25,75,37,75,40),this.canvas.ctx.fill()}}class po extends Xt{constructor(t,s){super(t),this.imgEl=s}draw(){super.draw(),this.canvas.ctx.drawImage(this.imgEl,0,0,this.particleSize,this.particleSize)}}class go{constructor(){this.cachedImages={}}createImageElement(t){const s=document.createElement("img");return s.setAttribute("src",t),s}getImageElement(t){return this.cachedImages[t]||(this.cachedImages[t]=this.createImageElement(t)),this.cachedImages[t]}getRandomParticle(t={}){const s=t.particles||[];return s.length<1?{}:s[Math.floor(Math.random()*s.length)]}getDefaults(t={}){return{type:t.defaultType||"circle",size:t.defaultSize||10,dropRate:t.defaultDropRate||10,colors:t.defaultColors||["DodgerBlue","OliveDrab","Gold","pink","SlateBlue","lightblue","Violet","PaleGreen","SteelBlue","SandyBrown","Chocolate","Crimson"],url:null}}create(t){const s=this.getDefaults(t),n=this.getRandomParticle(t),i=Object.assign(s,n),r=Ee(0,i.colors.length-1,!0);if(i.color=i.colors[r],i.type==="circle")return new uo(i);if(i.type==="rect")return new ao(i);if(i.type==="heart")return new ho(i);if(i.type==="image")return new po(i,this.getImageElement(i.url));throw Error(`Unknown particle type: "${i.type}"`)}}class mo{constructor(t){this.items=[],this.pool=[],this.particleOptions=t,this.particleFactory=new go}update(){const t=[],s=[];this.items.forEach(n=>{n.update(),n.pastBottom()?n.remove||(n.setup(this.particleOptions),t.push(n)):s.push(n)}),this.pool.push(...t),this.items=s}draw(){this.items.forEach(t=>t.draw())}add(){this.pool.length>0?this.items.push(this.pool.pop().setup(this.particleOptions)):this.items.push(this.particleFactory.create(this.particleOptions).setup(this.particleOptions))}refresh(){this.items.forEach(t=>{t.kill()}),this.pool=[]}}class zs{constructor(t){const s="confetti-canvas";if(t&&!(t instanceof HTMLCanvasElement))throw new Error("Element is not a valid HTMLCanvasElement");this.isDefault=!t,this.canvas=t||document.getElementById(s)||zs.createDefaultCanvas(s),this.ctx=this.canvas.getContext("2d")}static createDefaultCanvas(t){const s=document.createElement("canvas");return s.style.display="block",s.style.position="fixed",s.style.pointerEvents="none",s.style.top=0,s.style.width="100vw",s.style.height="100vh",s.id=t,document.querySelector("body").appendChild(s),s}get width(){return this.canvas.width}get height(){return this.canvas.height}clear(){this.ctx.setTransform(1,0,0,1,0,0),this.ctx.clearRect(0,0,this.width,this.height)}updateDimensions(){this.isDefault&&(this.width!==window.innerWidth||this.height!==window.innerHeight)&&(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight)}}class _o{constructor(){this.setDefaults()}setDefaults(){this.killed=!1,this.framesSinceDrop=0,this.canvas=null,this.canvasEl=null,this.W=0,this.H=0,this.particleManager=null,this.particlesPerFrame=0,this.wind=0,this.windSpeed=1,this.windSpeedMax=1,this.windChange=.01,this.windPosCoef=.002,this.animationId=null}getParticleOptions(t){const s={canvas:this.canvas,W:this.W,H:this.H,wind:this.wind,windPosCoef:this.windPosCoef,windSpeedMax:this.windSpeedMax,count:0};return Object.assign(s,t),s}createParticles(t={}){const s=this.getParticleOptions(t);this.particleManager=new mo(s)}getCanvasElementFromOptions(t){const{canvasId:s,canvasElement:n}=t;let i=n;if(n&&!(n instanceof HTMLCanvasElement))throw new Error("Invalid options: canvasElement is not a valid HTMLCanvasElement");if(s&&n)throw new Error("Invalid options: canvasId and canvasElement are mutually exclusive");if(s&&!i&&(i=document.getElementById(s)),s&&!(i instanceof HTMLCanvasElement))throw new Error(`Invalid options: element with id "${s}" is not a valid HTMLCanvasElement`);return i}start(t={}){this.remove();const s=this.getCanvasElementFromOptions(t);this.canvas=new zs(s),this.canvasEl=s,this.createParticles(t),this.setGlobalOptions(t),this.animationId=requestAnimationFrame(this.mainLoop.bind(this))}setGlobalOptions(t){this.particlesPerFrame=t.particlesPerFrame||2,this.windSpeedMax=t.windSpeedMax||1}stop(){this.killed=!0,this.particlesPerFrame=0}update(t){const s=this.getCanvasElementFromOptions(t);if(this.canvas&&s!==this.canvasEl){this.start(t);return}this.setGlobalOptions(t),this.particleManager&&(this.particleManager.particleOptions=this.getParticleOptions(t),this.particleManager.refresh())}remove(){this.stop(),this.animationId&&cancelAnimationFrame(this.animationId),this.canvas&&this.canvas.clear(),this.setDefaults()}mainLoop(t){this.canvas.updateDimensions(),this.canvas.clear(),this.windSpeed=Math.sin(t/8e3)*this.windSpeedMax,this.wind=this.particleManager.particleOptions.wind+=this.windChange;let s=this.framesSinceDrop*this.particlesPerFrame;for(;s>=1;)this.particleManager.add(),s-=1,this.framesSinceDrop=0;this.particleManager.update(),this.particleManager.draw(),(!this.killed||this.particleManager.items.length)&&(this.animationId=requestAnimationFrame(this.mainLoop.bind(this))),this.framesSinceDrop+=1}}const bo=si({__name:"ConfettiParty",setup(e){const t={defaultType:"rect",defaultSize:15,defaultColors:["DodgerBlue","OliveDrab","Gold","pink","SlateBlue","lightblue","Violet","PaleGreen","SteelBlue","SandyBrown","Chocolate","Crimson"]},s=new _o;return li(()=>{s.start(t),setTimeout(()=>{s.stop()},5e3)}),(n,i)=>(Ci(),yl("div"))}}),vo=si({__name:"App",setup(e){return(t,s)=>(Ci(),wl(bo))}}),xo=async()=>oo(vo).mount("#app-container");xo().then(()=>{console.log()});
18 | //# sourceMappingURL=welcome-BP_XNmLM.js.map
19 |
--------------------------------------------------------------------------------