From ff4622ba97866070cf9ae22a7089bd353426d59a Mon Sep 17 00:00:00 2001 From: Sven Wappler Date: Mon, 10 Aug 2026 22:31:14 +0200 Subject: [PATCH] TYPO3 v15 dev-main snapshot () --- .gitignore | 1 + .../Controller/DashboardAjaxController.php | 458 ++++++++++++++++++ Classes/Controller/DashboardController.php | 89 ++++ Classes/Dashboard.php | 159 ++++++ Classes/DashboardEntry.php | 172 +++++++ Classes/DashboardInitializationService.php | 184 +++++++ Classes/DashboardPreset.php | 88 ++++ Classes/DashboardPresetRegistry.php | 54 +++ .../DashboardWidgetPass.php | 109 +++++ Classes/Dto/Dashboard.php | 41 ++ Classes/Dto/WidgetConfiguration.php | 41 ++ Classes/Dto/WidgetData.php | 51 ++ Classes/Dto/WidgetPosition.php | 43 ++ .../AfterBackendPageRenderEventListener.php | 36 ++ Classes/Exception/InvalidRssFeedException.php | 22 + Classes/Factory/WidgetSettingsFactory.php | 75 +++ .../Persistence/DashboardCreationEnricher.php | 50 ++ Classes/Repository/DashboardRepository.php | 195 ++++++++ Classes/ServiceProvider.php | 231 +++++++++ .../ViewHelpers/Widget/ButtonViewHelper.php | 74 +++ Classes/WidgetApi.php | 40 ++ Classes/WidgetGroup.php | 43 ++ Classes/WidgetGroupInitializationService.php | 74 +++ Classes/WidgetGroupRegistry.php | 39 ++ Classes/WidgetRegistry.php | 139 ++++++ Classes/Widgets/AdditionalCssInterface.php | 30 ++ .../Widgets/AdditionalJavaScriptInterface.php | 30 ++ Classes/Widgets/AdminOnlyWidgetInterface.php | 23 + Classes/Widgets/BarChartWidget.php | 110 +++++ Classes/Widgets/BookmarksWidget.php | 133 +++++ Classes/Widgets/ButtonProviderInterface.php | 41 ++ .../Widgets/ChartDataProviderInterface.php | 34 ++ Classes/Widgets/CtaWidget.php | 73 +++ Classes/Widgets/DoughnutChartWidget.php | 100 ++++ .../Widgets/ElementAttributesInterface.php | 29 ++ Classes/Widgets/EventDataInterface.php | 29 ++ Classes/Widgets/JavaScriptInterface.php | 31 ++ Classes/Widgets/LatestBeLoginsWidget.php | 83 ++++ Classes/Widgets/LatestChangedPagesWidget.php | 284 +++++++++++ Classes/Widgets/ListDataProviderInterface.php | 30 ++ Classes/Widgets/ListWidget.php | 72 +++ .../NumberWithIconDataProviderInterface.php | 29 ++ Classes/Widgets/NumberWithIconWidget.php | 75 +++ Classes/Widgets/Provider/ButtonProvider.php | 47 ++ .../Provider/LatestBeLoginsDataProvider.php | 78 +++ .../NumberOfFailedLoginsDataProvider.php | 58 +++ .../Widgets/Provider/SysLogButtonProvider.php | 63 +++ .../Provider/SysLogErrorsDataProvider.php | 99 ++++ .../Provider/TypeOfUsersChartDataProvider.php | 66 +++ .../Widgets/RequestAwareWidgetInterface.php | 31 ++ Classes/Widgets/RssWidget.php | 195 ++++++++ .../Widgets/T3GeneralInformationWidget.php | 68 +++ Classes/Widgets/WidgetConfiguration.php | 89 ++++ .../Widgets/WidgetConfigurationInterface.php | 66 +++ Classes/Widgets/WidgetContext.php | 44 ++ Classes/Widgets/WidgetInterface.php | 38 ++ Classes/Widgets/WidgetRendererInterface.php | 40 ++ Classes/Widgets/WidgetResult.php | 39 ++ Configuration/Backend/AjaxRoutes.php | 85 ++++ Configuration/Backend/DashboardPresets.php | 18 + .../Backend/DashboardWidgetGroups.php | 24 + Configuration/Backend/Modules.php | 22 + Configuration/JavaScriptModules.php | 19 + Configuration/Services.php | 12 + Configuration/Services.yaml | 252 ++++++++++ Configuration/TCA/Overrides/be_groups.php | 26 + Configuration/TCA/be_dashboards.php | 63 +++ Documentation/Configuration/Index.rst | 20 + .../PermissionHandlingOfWidgets.rst | 17 + .../Configuration/WidgetGroupCreation.rst | 45 ++ Documentation/Configuration/WidgetPresets.rst | 124 +++++ .../Configuration/WidgetRegistration.rst | 326 +++++++++++++ .../Configuration/WidgetSettings.rst | 59 +++ .../Configuration/WidgetTemplate.rst | 26 + Documentation/Developer/AddingButtons.rst | 111 +++++ .../Developer/ConfigurableWidgets.rst | 34 ++ .../Developer/GraphWidgetImplementation.rst | 174 +++++++ Documentation/Developer/Index.rst | 25 + Documentation/Developer/Interfaces.rst | 178 +++++++ Documentation/Developer/MakeRefreshable.rst | 83 ++++ .../Developer/WidgetImplementation.rst | 195 ++++++++ .../_codesnippets/_ConfigurableWidget.php.inc | 43 ++ Documentation/Editor/Index.rst | 167 +++++++ Documentation/Images/AccessRestriction.png | Bin 0 -> 42016 bytes Documentation/Images/AddWidget.png | Bin 0 -> 21703 bytes .../Images/DashboardConfiguration.png | Bin 0 -> 19432 bytes Documentation/Images/DashboardPosition.png | Bin 0 -> 50580 bytes Documentation/Images/DashboardTabs.png | Bin 0 -> 10458 bytes Documentation/Images/DashboardWizard.png | Bin 0 -> 23802 bytes Documentation/Images/InstallActivate.png | Bin 0 -> 21091 bytes Documentation/Images/WidgetDelete.png | Bin 0 -> 16237 bytes Documentation/Images/WidgetMove.png | Bin 0 -> 8787 bytes Documentation/Images/WidgetWizard.png | Bin 0 -> 29718 bytes Documentation/Includes.rst.txt | 1 + Documentation/Index.rst | 55 +++ Documentation/Installation/Index.rst | 54 +++ Documentation/Introduction/Index.rst | 16 + ...renceRegistrationAndImplementation.rst.txt | 13 + Documentation/Sitemap.rst | 11 + Documentation/Widgets/BarChartWidget.rst | 73 +++ Documentation/Widgets/CTAButtonWidget.rst | 72 +++ Documentation/Widgets/DoughnutChartWidget.rst | 70 +++ Documentation/Widgets/Index.rst | 17 + Documentation/Widgets/ListWidget.rst | 58 +++ .../Widgets/NumberWithIconWidget.rst | 84 ++++ .../Widgets/Options/RefreshAvailable.rst.txt | 9 + Documentation/Widgets/RssWidget.rst | 128 +++++ .../_codesnippets/_rsswidget-services.yaml | 28 ++ Documentation/guides.xml | 21 + LICENSE.txt | 339 +++++++++++++ README.rst | 10 + Resources/Private/Language/db.xlf | 17 + Resources/Private/Language/locallang.xlf | 329 +++++++++++++ .../Language/locallang_widget_bookmarks.xlf | 32 ++ .../Private/Language/locallang_widget_rss.xlf | 56 +++ Resources/Private/Language/module.xlf | 17 + .../Private/Layouts/Widget/Widget.fluid.html | 11 + .../Private/Partials/Widget/Button.fluid.html | 7 + .../Templates/Dashboard/Main.fluid.html | 8 + .../Widget/BookmarksWidget.fluid.html | 14 + .../Templates/Widget/ChartWidget.fluid.html | 13 + .../Templates/Widget/CtaWidget.fluid.html | 13 + .../Widget/LatestBeLoginsWidget.html | 66 +++ .../LatestChangedPagesWidget.fluid.html | 122 +++++ .../Templates/Widget/ListWidget.fluid.html | 17 + .../Widget/NumberWithIconWidget.fluid.html | 21 + .../Templates/Widget/RssWidget.fluid.html | 36 ++ .../T3GeneralInformationWidget.fluid.html | 27 ++ Resources/Public/Css/Modal/style.css | 24 + Resources/Public/Css/dashboard.css | 84 ++++ Resources/Public/Icons/Extension.svg | 1 + .../Public/JavaScript/Contrib/chartjs.js | 29 ++ .../Public/JavaScript/chart-initializer.js | 13 + Resources/Public/JavaScript/dashboard.js | 13 + .../widget/bookmarks-widget-element.js | 13 + composer.json | 61 +++ ext_localconf.php | 22 + ext_tables.sql | 7 + 138 files changed, 9045 insertions(+) create mode 100644 .gitignore create mode 100644 Classes/Controller/DashboardAjaxController.php create mode 100644 Classes/Controller/DashboardController.php create mode 100644 Classes/Dashboard.php create mode 100644 Classes/DashboardEntry.php create mode 100644 Classes/DashboardInitializationService.php create mode 100644 Classes/DashboardPreset.php create mode 100644 Classes/DashboardPresetRegistry.php create mode 100644 Classes/DependencyInjection/DashboardWidgetPass.php create mode 100644 Classes/Dto/Dashboard.php create mode 100644 Classes/Dto/WidgetConfiguration.php create mode 100644 Classes/Dto/WidgetData.php create mode 100644 Classes/Dto/WidgetPosition.php create mode 100644 Classes/EventListener/AfterBackendPageRenderEventListener.php create mode 100644 Classes/Exception/InvalidRssFeedException.php create mode 100644 Classes/Factory/WidgetSettingsFactory.php create mode 100644 Classes/Persistence/DashboardCreationEnricher.php create mode 100644 Classes/Repository/DashboardRepository.php create mode 100644 Classes/ServiceProvider.php create mode 100644 Classes/ViewHelpers/Widget/ButtonViewHelper.php create mode 100644 Classes/WidgetApi.php create mode 100644 Classes/WidgetGroup.php create mode 100644 Classes/WidgetGroupInitializationService.php create mode 100644 Classes/WidgetGroupRegistry.php create mode 100644 Classes/WidgetRegistry.php create mode 100644 Classes/Widgets/AdditionalCssInterface.php create mode 100644 Classes/Widgets/AdditionalJavaScriptInterface.php create mode 100644 Classes/Widgets/AdminOnlyWidgetInterface.php create mode 100644 Classes/Widgets/BarChartWidget.php create mode 100644 Classes/Widgets/BookmarksWidget.php create mode 100644 Classes/Widgets/ButtonProviderInterface.php create mode 100644 Classes/Widgets/ChartDataProviderInterface.php create mode 100644 Classes/Widgets/CtaWidget.php create mode 100644 Classes/Widgets/DoughnutChartWidget.php create mode 100644 Classes/Widgets/ElementAttributesInterface.php create mode 100644 Classes/Widgets/EventDataInterface.php create mode 100644 Classes/Widgets/JavaScriptInterface.php create mode 100644 Classes/Widgets/LatestBeLoginsWidget.php create mode 100644 Classes/Widgets/LatestChangedPagesWidget.php create mode 100644 Classes/Widgets/ListDataProviderInterface.php create mode 100644 Classes/Widgets/ListWidget.php create mode 100644 Classes/Widgets/NumberWithIconDataProviderInterface.php create mode 100644 Classes/Widgets/NumberWithIconWidget.php create mode 100644 Classes/Widgets/Provider/ButtonProvider.php create mode 100644 Classes/Widgets/Provider/LatestBeLoginsDataProvider.php create mode 100644 Classes/Widgets/Provider/NumberOfFailedLoginsDataProvider.php create mode 100644 Classes/Widgets/Provider/SysLogButtonProvider.php create mode 100644 Classes/Widgets/Provider/SysLogErrorsDataProvider.php create mode 100644 Classes/Widgets/Provider/TypeOfUsersChartDataProvider.php create mode 100644 Classes/Widgets/RequestAwareWidgetInterface.php create mode 100644 Classes/Widgets/RssWidget.php create mode 100644 Classes/Widgets/T3GeneralInformationWidget.php create mode 100644 Classes/Widgets/WidgetConfiguration.php create mode 100644 Classes/Widgets/WidgetConfigurationInterface.php create mode 100644 Classes/Widgets/WidgetContext.php create mode 100644 Classes/Widgets/WidgetInterface.php create mode 100644 Classes/Widgets/WidgetRendererInterface.php create mode 100644 Classes/Widgets/WidgetResult.php create mode 100644 Configuration/Backend/AjaxRoutes.php create mode 100644 Configuration/Backend/DashboardPresets.php create mode 100644 Configuration/Backend/DashboardWidgetGroups.php create mode 100644 Configuration/Backend/Modules.php create mode 100644 Configuration/JavaScriptModules.php create mode 100644 Configuration/Services.php create mode 100644 Configuration/Services.yaml create mode 100644 Configuration/TCA/Overrides/be_groups.php create mode 100644 Configuration/TCA/be_dashboards.php create mode 100644 Documentation/Configuration/Index.rst create mode 100644 Documentation/Configuration/PermissionHandlingOfWidgets.rst create mode 100644 Documentation/Configuration/WidgetGroupCreation.rst create mode 100644 Documentation/Configuration/WidgetPresets.rst create mode 100644 Documentation/Configuration/WidgetRegistration.rst create mode 100644 Documentation/Configuration/WidgetSettings.rst create mode 100644 Documentation/Configuration/WidgetTemplate.rst create mode 100644 Documentation/Developer/AddingButtons.rst create mode 100644 Documentation/Developer/ConfigurableWidgets.rst create mode 100644 Documentation/Developer/GraphWidgetImplementation.rst create mode 100644 Documentation/Developer/Index.rst create mode 100644 Documentation/Developer/Interfaces.rst create mode 100644 Documentation/Developer/MakeRefreshable.rst create mode 100644 Documentation/Developer/WidgetImplementation.rst create mode 100644 Documentation/Developer/_codesnippets/_ConfigurableWidget.php.inc create mode 100644 Documentation/Editor/Index.rst create mode 100644 Documentation/Images/AccessRestriction.png create mode 100644 Documentation/Images/AddWidget.png create mode 100644 Documentation/Images/DashboardConfiguration.png create mode 100644 Documentation/Images/DashboardPosition.png create mode 100644 Documentation/Images/DashboardTabs.png create mode 100644 Documentation/Images/DashboardWizard.png create mode 100644 Documentation/Images/InstallActivate.png create mode 100644 Documentation/Images/WidgetDelete.png create mode 100644 Documentation/Images/WidgetMove.png create mode 100644 Documentation/Images/WidgetWizard.png create mode 100644 Documentation/Includes.rst.txt create mode 100644 Documentation/Index.rst create mode 100644 Documentation/Installation/Index.rst create mode 100644 Documentation/Introduction/Index.rst create mode 100644 Documentation/Shared/DifferenceRegistrationAndImplementation.rst.txt create mode 100644 Documentation/Sitemap.rst create mode 100644 Documentation/Widgets/BarChartWidget.rst create mode 100644 Documentation/Widgets/CTAButtonWidget.rst create mode 100644 Documentation/Widgets/DoughnutChartWidget.rst create mode 100644 Documentation/Widgets/Index.rst create mode 100644 Documentation/Widgets/ListWidget.rst create mode 100644 Documentation/Widgets/NumberWithIconWidget.rst create mode 100644 Documentation/Widgets/Options/RefreshAvailable.rst.txt create mode 100644 Documentation/Widgets/RssWidget.rst create mode 100644 Documentation/Widgets/_codesnippets/_rsswidget-services.yaml create mode 100644 Documentation/guides.xml create mode 100644 LICENSE.txt create mode 100644 README.rst create mode 100644 Resources/Private/Language/db.xlf create mode 100644 Resources/Private/Language/locallang.xlf create mode 100644 Resources/Private/Language/locallang_widget_bookmarks.xlf create mode 100644 Resources/Private/Language/locallang_widget_rss.xlf create mode 100644 Resources/Private/Language/module.xlf create mode 100644 Resources/Private/Layouts/Widget/Widget.fluid.html create mode 100644 Resources/Private/Partials/Widget/Button.fluid.html create mode 100644 Resources/Private/Templates/Dashboard/Main.fluid.html create mode 100644 Resources/Private/Templates/Widget/BookmarksWidget.fluid.html create mode 100644 Resources/Private/Templates/Widget/ChartWidget.fluid.html create mode 100644 Resources/Private/Templates/Widget/CtaWidget.fluid.html create mode 100644 Resources/Private/Templates/Widget/LatestBeLoginsWidget.html create mode 100644 Resources/Private/Templates/Widget/LatestChangedPagesWidget.fluid.html create mode 100644 Resources/Private/Templates/Widget/ListWidget.fluid.html create mode 100644 Resources/Private/Templates/Widget/NumberWithIconWidget.fluid.html create mode 100644 Resources/Private/Templates/Widget/RssWidget.fluid.html create mode 100644 Resources/Private/Templates/Widget/T3GeneralInformationWidget.fluid.html create mode 100644 Resources/Public/Css/Modal/style.css create mode 100644 Resources/Public/Css/dashboard.css create mode 100644 Resources/Public/Icons/Extension.svg create mode 100644 Resources/Public/JavaScript/Contrib/chartjs.js create mode 100644 Resources/Public/JavaScript/chart-initializer.js create mode 100644 Resources/Public/JavaScript/dashboard.js create mode 100644 Resources/Public/JavaScript/widget/bookmarks-widget-element.js create mode 100644 composer.json create mode 100644 ext_localconf.php create mode 100644 ext_tables.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/Classes/Controller/DashboardAjaxController.php b/Classes/Controller/DashboardAjaxController.php new file mode 100644 index 0000000..6d7a328 --- /dev/null +++ b/Classes/Controller/DashboardAjaxController.php @@ -0,0 +1,458 @@ +dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $dashboards = []; + foreach ($availableDashboards as $dashboard) { + $dashboard->initializeWidgets($request); + $dashboards[] = $dashboard->getTransferData(); + } + + return new JsonResponse($dashboards); + } + + public function addDashboard(ServerRequestInterface $request): ResponseInterface + { + $presetIdentifier = (string)($request->getParsedBody()['preset'] ?? ''); + $dashboardPreset = $this->dashboardPresetRegistry->getDashboardPresets()[$presetIdentifier] ?? null; + if (!$dashboardPreset instanceof DashboardPreset) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Invalid dashboard preset!', + ]); + } + + $dashboardEntity = $this->dashboardRepository->create( + $dashboardPreset, + (int)$this->getBackendUser()->user['uid'], + (string)($request->getParsedBody()['title'] ?? '') + ); + + $dashboardEntity->initializeWidgets($request); + + return new JsonResponse([ + 'status' => 'ok', + 'dashboard' => $dashboardEntity->getTransferData(), + ]); + } + + public function editDashboard(ServerRequestInterface $request): ResponseInterface + { + $dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? ''); + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + + if (!in_array($dashboardEntity, $availableDashboards)) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Dashboard is not available!', + ]); + } + + $this->dashboardRepository->updateDashboardSettings( + $dashboardIdentifier, + [ + 'title' => (string)($request->getParsedBody()['title'] ?? ''), + ] + ); + + // Fetch updated Dashboard + $dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + $dashboardEntity->initializeWidgets($request); + + return new JsonResponse([ + 'status' => 'ok', + 'dashboard' => $dashboardEntity->getTransferData(), + ]); + } + + public function updateDashboard(ServerRequestInterface $request): ResponseInterface + { + $dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? ''); + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + + if (!in_array($dashboardEntity, $availableDashboards)) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Dashboard is not available!', + ]); + } + + $widgets = $request->getParsedBody()['widgets'] ?? []; + $data = []; + foreach ($widgets as $widget) { + $data[$widget['identifier']] = [ + 'identifier' => $widget['type'], + ]; + } + + // positions + $widgetPositions = $request->getParsedBody()['widgetPositions'] ?? []; + foreach ($widgetPositions as $columnCount => $widgets) { + foreach ($widgets as $widget) { + if (!isset($widget['identifier']) || !isset($widget['height']) || !isset($widget['width']) || !isset($widget['x']) || !isset($widget['y'])) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Invalid widget positions!', + ]); + } + $identifier = $widget['identifier'] ?? ''; + unset($widget['identifier']); + $data[$identifier]['positions'][$columnCount] = array_map('intval', $widget); + } + } + + // settings + $dashboardEntity->initializeWidgets($request); + foreach ($widgets as $widget) { + $dashboardWidget = $dashboardEntity->getWidget($widget['identifier']); + if ($dashboardWidget) { + $data[$widget['identifier']]['settings'] = $dashboardWidget->getRawConfig()['settings'] ?? []; + } + } + + $this->dashboardRepository->updateWidgetConfig($dashboardEntity, $data); + + // Fetch updated Dashboard + $dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + $dashboardEntity->initializeWidgets($request); + + return new JsonResponse([ + 'status' => 'ok', + 'dashboard' => $dashboardEntity->getTransferData(), + ]); + } + + public function deleteDashboard(ServerRequestInterface $request): ResponseInterface + { + $dashboardIdentifier = (string)($request->getParsedBody()['identifier'] ?? ''); + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + + if (!in_array($dashboard, $availableDashboards)) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Dashboard is not available!', + ]); + } + + $this->dashboardRepository->delete($dashboard); + return new JsonResponse([ + 'status' => 'ok', + ]); + } + + public function getPresets(ServerRequestInterface $request): ResponseInterface + { + $presets = $this->dashboardPresetRegistry->getDashboardPresets(); + return new JsonResponse($presets); + } + + public function getCategories(ServerRequestInterface $request): ResponseInterface + { + $widgetGroups = $this->widgetGroupInitializationService->buildWidgetGroupsConfiguration(); + return new JsonResponse($widgetGroups); + } + + public function getWidget(ServerRequestInterface $request): ResponseInterface + { + $widgetIdentifier = (string)($request->getQueryParams()['widget'] ?? ''); + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $widgets = []; + + foreach ($availableDashboards as $dashboard) { + $dashboard->initializeWidgets($request); + foreach ($dashboard->getWidgets() as $dashboardEntry) { + $widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry; + } + } + + $dashboardWidget = $widgets[$widgetIdentifier] ?? null; + if ($dashboardWidget === null) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Widget does not exist!', + ]); + } + + return new JsonResponse([ + 'status' => 'ok', + 'widget' => $dashboardWidget->getTransferWidgetData()->jsonSerialize(), + ]); + } + + public function getWidgetSettings(ServerRequestInterface $request): ResponseInterface + { + $widgetIdentifier = (string)($request->getQueryParams()['widget'] ?? ''); + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $widgets = []; + + foreach ($availableDashboards as $dashboard) { + $dashboard->initializeWidgets($request); + foreach ($dashboard->getWidgets() as $dashboardEntry) { + $widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry; + } + } + + $dashboardWidget = $widgets[$widgetIdentifier] ?? null; + if ($dashboardWidget === null) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Widget does not exist!', + ]); + } + + $categories = [ + new Category( + key: $dashboardWidget->getType(), + label: $this->getLanguageService()->sL($dashboardWidget->getTitle()), + description: $this->getLanguageService()->sL($dashboardWidget->getDescription()), + icon: $dashboardWidget->getIconIdentifier(), + settings: array_map( + fn(SettingDefinition $definition): EditableSetting => new EditableSetting( + definition: $this->resolveSettingLabels($definition), + value: $dashboardWidget->getSettings()->get($definition->key), + systemDefault: $definition->default, + typeImplementation: $this->settingsTypeRegistry->get($definition->type)->getJavaScriptModule(), + ), + array_values(array_filter($dashboardWidget->getSettingsDefinitions(), fn(SettingDefinition $settingDefinition) => !$settingDefinition->readonly)) + ), + ), + ]; + + return new JsonResponse([ + 'status' => 'ok', + 'categories' => json_encode($categories), + ]); + } + + public function updateWidgetSettings(ServerRequestInterface $request): ResponseInterface + { + // Check if identifier is available + $widgetIdentifier = trim((string)($request->getParsedBody()['widget'] ?? '')); + if ($widgetIdentifier === '') { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Widget is not available!', + ]); + } + + // Check for widget + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $targetDashboard = null; + $targetWidget = null; + foreach ($availableDashboards as $dashboard) { + $dashboard->initializeWidgets($request); + if ($dashboard->getWidget($widgetIdentifier)) { + $targetDashboard = $dashboard; + $targetWidget = $dashboard->getWidget($widgetIdentifier); + break; + } + } + + if ($targetWidget === null) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Widget does not exist!', + ]); + } + if ($targetDashboard === null) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Dashboard does not exist!', + ]); + } + + $rawSettings = $request->getParsedBody()['settings'] ?? []; + $widgetData = []; + foreach ($targetDashboard->getWidgets() as $widget) { + $widgetData[$widget->getIdentifier()] = $widget->getRawConfig(); + if ($targetWidget->getIdentifier() === $widget->getIdentifier()) { + + $currentSettings = $widget->getRawConfig()['settings'] ?? []; + $newSettings = $this->widgetSettingsFactory->createSettingsFromFormData($rawSettings, $widget->getSettingsDefinitions()); + $defaultSettings = $this->widgetSettingsFactory->createSettings($widget->getType(), [], $widget->getSettingsDefinitions()); + $diff = SettingsDiff::create( + $currentSettings, + $newSettings, + $defaultSettings, + ); + if ($diff->changes === [] && $diff->deletions === []) { + return new JsonResponse([ + 'status' => 'info', + 'message' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.settings.unchanged'), + ]); + } + $widgetData[$widget->getIdentifier()]['settings'] = $diff->settings; + } + } + + $this->dashboardRepository->updateWidgetConfig($targetDashboard, $widgetData); + + // Fetch updated Dashboard + $dashboardEntity = $this->dashboardRepository->getDashboardByIdentifier($targetDashboard->getIdentifier()); + $dashboardEntity->initializeWidgets($request); + + $returnWidget = $dashboardEntity->getWidget($targetWidget->getIdentifier()); + if ($returnWidget === null) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Widget does not exist!', + ]); + } + + return new JsonResponse([ + 'status' => 'ok', + ]); + } + + public function addWidget(ServerRequestInterface $request): ResponseInterface + { + $dashboardIdentifier = (string)($request->getParsedBody()['dashboard'] ?? ''); + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + + if (!in_array($dashboard, $availableDashboards)) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Dashboard is not available!', + ]); + } + + $widgetType = (string)($request->getParsedBody()['type'] ?? ''); + if ($widgetType === '') { + throw new \InvalidArgumentException('Argument "widget" not set.', 1714987384); + } + $widgets = $dashboard->getWidgetConfig(); + $widgetIdentifier = sha1($widgetType . '-' . time()); + $widgets[$widgetIdentifier] = ['identifier' => $widgetType]; + $this->dashboardRepository->updateWidgetConfig($dashboard, $widgets); + + // Fetch updated Dashboard + $dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + $dashboard->initializeWidgets($request); + + $widgets = []; + foreach ($dashboard->getWidgets() as $dashboardEntry) { + $widgets[$dashboardEntry->getIdentifier()] = $dashboardEntry; + } + + $dashboardWidget = $widgets[$widgetIdentifier] ?? null; + if ($dashboardWidget === null) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Widget is not available!', + ]); + } + + return new JsonResponse([ + 'status' => 'ok', + 'widget' => $dashboardWidget->getTransferWidgetData()->jsonSerialize(), + ]); + } + + public function removeWidget(ServerRequestInterface $request): ResponseInterface + { + $dashboardIdentifier = (string)($request->getParsedBody()['dashboard'] ?? ''); + $availableDashboards = $this->dashboardRepository->getDashboardsForUser($this->getBackendUser()->getUserId()); + $dashboard = $this->dashboardRepository->getDashboardByIdentifier($dashboardIdentifier); + + if (!in_array($dashboard, $availableDashboards)) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Dashboard is not available!', + ]); + } + + $widgetIdentifier = (string)($request->getParsedBody()['identifier'] ?? ''); + $widgets = $dashboard->getWidgetConfig(); + if ($widgetIdentifier === '' || !array_key_exists($widgetIdentifier, $widgets)) { + return new JsonResponse([ + 'status' => 'error', + 'message' => 'Widget is not available!', + ]); + } + + unset($widgets[$widgetIdentifier]); + $this->dashboardRepository->updateWidgetConfig($dashboard, $widgets); + + return new JsonResponse([ + 'status' => 'ok', + ]); + } + + private function resolveSettingLabels(SettingDefinition $definition): SettingDefinition + { + $languageService = $this->getLanguageService(); + return new SettingDefinition(...[ + ...get_object_vars($definition), + 'label' => $languageService->sL($definition->label), + 'description' => $definition->description !== null ? $languageService->sL($definition->description) : null, + 'enum' => array_map(static fn(string $label): string => $languageService->sL($label), $definition->enum), + ]); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Controller/DashboardController.php b/Classes/Controller/DashboardController.php new file mode 100644 index 0000000..b4f99e3 --- /dev/null +++ b/Classes/Controller/DashboardController.php @@ -0,0 +1,89 @@ +dashboardInitializationService->initializeDashboards($request, $this->getBackendUser()); + + $view = $this->moduleTemplateFactory->create($request); + $this->preparePageRenderer(); + $this->addFrontendResources(); + $view->setTitle($this->getLanguageService()->translate('title', 'dashboard.module')); + $view->getDocHeaderComponent()->disable(); + + return $view->renderResponse('Dashboard/Main'); + } + + /** + * Adds CSS and JS files that are necessary for widgets to the page renderer + */ + protected function addFrontendResources(): void + { + $javaScriptRenderer = $this->pageRenderer->getJavaScriptRenderer(); + foreach ($this->dashboardInitializationService->getJavaScriptModuleInstructions() as $instruction) { + $javaScriptRenderer->addJavaScriptModuleInstruction($instruction); + } + foreach ($this->dashboardInitializationService->getCssFiles() as $cssFile) { + $this->pageRenderer->addCssFile($cssFile); + } + foreach ($this->dashboardInitializationService->getJsFiles() as $jsFile) { + $this->pageRenderer->addJsFile($jsFile); + } + } + + /** + * Add the CSS and JS of the dashboard module to the page renderer + */ + protected function preparePageRenderer(): void + { + $this->pageRenderer->loadJavaScriptModule('@typo3/dashboard/dashboard.js'); + $this->pageRenderer->addCssFile('EXT:dashboard/Resources/Public/Css/dashboard.css'); + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Dashboard.php b/Classes/Dashboard.php new file mode 100644 index 0000000..d108e5d --- /dev/null +++ b/Classes/Dashboard.php @@ -0,0 +1,159 @@ + + */ + protected array $widgets = []; + + protected ?object $widgetPositions = null; + + /** + * @param array> $widgetConfig + */ + public function __construct( + protected readonly string $identifier, + protected readonly string $title, + protected readonly array $widgetConfig, + protected readonly WidgetRegistry $widgetRegistry, + protected readonly WidgetSettingsFactory $widgetSettingsFactory, + protected readonly ContainerInterface $container, + ) {} + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getTitle(): string + { + return $this->getLanguageService()->sL($this->title) ?: $this->title; + } + + public function getWidgetConfig(): array + { + return $this->widgetConfig; + } + + /** + * @return array + */ + public function getWidgets(): array + { + return $this->widgets; + } + + public function getWidget(string $identifier): ?DashboardEntry + { + return $this->widgets[$identifier] ?? null; + } + + public function getWidgetPositions(): object + { + return $this->widgetPositions ?? new \stdClass(); + } + + /** + * This will return a list of all widgets of the current dashboard object. It will only include available + * widgets and will add the initialized object of the widget itself + */ + public function initializeWidgets(ServerRequestInterface $request): void + { + $availableWidgets = $this->widgetRegistry->getAvailableWidgets(); + $this->widgetPositions = new \stdClass(); + foreach ($this->widgetConfig as $hash => $widgetConfig) { + $widgetConfigIdentifier = $widgetConfig['identifier'] ?? ''; + if ($widgetConfigIdentifier !== '' && array_key_exists($widgetConfigIdentifier, $availableWidgets)) { + + // Widget (Renderer) Instance + $widgetRenderer = $this->widgetRegistry->getAvailableWidget($request, $widgetConfigIdentifier); + + // Dashboard Entry with Widget Context + $this->widgets[$hash] = new DashboardEntry( + context: new WidgetContext( + identifier: $hash, + rawData: $widgetConfig, + configuration: $availableWidgets[$widgetConfigIdentifier], + settings: $widgetRenderer instanceof WidgetRendererInterface ? $this->widgetSettingsFactory->createSettings( + $widgetConfigIdentifier, + $widgetConfig['settings'] ?? [], + $widgetRenderer->getSettingsDefinitions(), + ) : new Settings([]), + request: $request, + ), + renderer: $widgetRenderer, + ); + + // Widget Positions + $positions = $widgetConfig['positions'] ?? []; + foreach ($positions as $columnCount => $position) { + if (!isset($position['height']) || !isset($position['width']) || !isset($position['x']) || !isset($position['y'])) { + continue; + } + if (!isset($this->widgetPositions->{$columnCount})) { + $this->widgetPositions->{$columnCount} = []; + } + $this->widgetPositions->{$columnCount}[] = [ + 'identifier' => $hash, + 'height' => (int)$position['height'], + 'width' => (int)$position['width'], + 'x' => (int)$position['x'], + 'y' => (int)$position['y'], + ]; + } + } + } + foreach (array_keys(get_object_vars($this->widgetPositions)) as $columnCount) { + usort( + $this->widgetPositions->{$columnCount}, + static fn(array $a, array $b): int => $a['y'] !== $b['y'] ? $a['y'] - $b['y'] : $a['x'] - $b['x'] + ); + } + } + + public function getTransferData(): TransferDashboard + { + return new TransferDashboard( + identifier: $this->getIdentifier(), + title: $this->getTitle(), + widgets: array_values(array_map(fn(DashboardEntry $entry): TransferWidgetConfiguration => $entry->getTransferWidgetConfiguration(), $this->getWidgets())), + widgetPositions: $this->getWidgetPositions(), + ); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/DashboardEntry.php b/Classes/DashboardEntry.php new file mode 100644 index 0000000..28c25ac --- /dev/null +++ b/Classes/DashboardEntry.php @@ -0,0 +1,172 @@ +context->identifier; + } + + public function getType(): string + { + return $this->context->configuration->getIdentifier(); + } + + public function getTitle(): string + { + return $this->context->configuration->getTitle(); + } + + public function getDescription(): string + { + return $this->context->configuration->getDescription(); + } + + public function getIconIdentifier(): string + { + return $this->context->configuration->getIconIdentifier(); + } + + public function getHeight(): string + { + return $this->context->configuration->getHeight(); + } + + public function getWidth(): string + { + return $this->context->configuration->getWidth(); + } + + public function getSettings(): SettingsInterface + { + return $this->context->settings; + } + + public function getRequest(): ServerRequestInterface + { + return $this->context->request; + } + + public function getEventData(): array + { + return ($this->renderer instanceof EventDataInterface) ? $this->renderer->getEventData() : []; + } + + public function getTransferWidgetConfiguration(): TransferWidgetConfiguration + { + return new TransferWidgetConfiguration( + identifier: $this->getIdentifier(), + type: $this->getType(), + height: $this->getHeight(), + width: $this->getWidth(), + ); + } + + public function getTransferWidgetData(): TransferWidgetData + { + $result = $this->render(); + return new TransferWidgetData( + identifier: $this->getIdentifier(), + type: $this->getType(), + height: $this->getHeight(), + width: $this->getWidth(), + label: $result->label ?? $this->getLanguageService()->sL($this->getTitle()), + content: $result->content, + eventdata: $this->getEventData(), + refreshable: $result->refreshable, + configurable: ( + $this->renderer instanceof WidgetRendererInterface + && array_filter($this->renderer->getSettingsDefinitions(), fn($definition) => !$definition->readonly) !== [] + ) + ); + } + + /** + * @return SettingDefinition[] + */ + public function getSettingsDefinitions(): array + { + if ($this->renderer instanceof WidgetRendererInterface) { + return $this->renderer->getSettingsDefinitions(); + } + + return []; + } + + public function getRawConfig(): array + { + return $this->context->rawData; + } + + private function render(): WidgetResult + { + try { + if ($this->renderer instanceof WidgetRendererInterface) { + return $this->renderer->renderWidget($this->context); + } + // Map legacy WidgetInterface to "new" WidgetResult + return new WidgetResult( + content: $this->renderer->renderWidgetContent(), + refreshable: $this->renderer->getOptions()['refreshAvailable'] ?? false, + ); + + } catch (\Exception) { + return new WidgetResult( + content: sprintf('
%s
', $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget.error')), + refreshable: true, + ); + } + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/DashboardInitializationService.php b/Classes/DashboardInitializationService.php new file mode 100644 index 0000000..e587718 --- /dev/null +++ b/Classes/DashboardInitializationService.php @@ -0,0 +1,184 @@ + + */ + protected array $javaScriptModuleInstructions = []; + private array $jsFiles = []; + private array $cssFiles = []; + + public function __construct( + private readonly DashboardRepository $dashboardRepository, + private readonly DashboardPresetRegistry $dashboardPresetRegistry, + private readonly WidgetRegistry $widgetRegistry, + ) {} + + public function initializeDashboards(ServerRequestInterface $request, BackendUserAuthentication $user): void + { + $this->defineCurrentDashboard($user); + $this->defineResourcesOfWidgets(); + } + + protected function defineCurrentDashboard(BackendUserAuthentication $user): Dashboard + { + $currentDashboard = $this->dashboardRepository->getDashboardByIdentifier($this->loadCurrentDashboard($user)); + if ($currentDashboard === null) { + $dashboards = $this->getDashboardsForUser($user); + /** @var Dashboard $currentDashboard */ + $currentDashboard = reset($dashboards); + $this->saveCurrentDashboard($user, $currentDashboard->getIdentifier()); + } + + return $currentDashboard; + } + + protected function createDefaultDashboards(BackendUserAuthentication $user): array + { + $dashboardsForUser = []; + + $userConfig = $user->getTSConfig(); + $dashboardsToCreate = GeneralUtility::trimExplode( + ',', + $userConfig['options.']['dashboard.']['dashboardPresetsForNewUsers'] ?? 'default' + ); + + foreach ($this->dashboardPresetRegistry->getDashboardPresets() as $dashboardPreset) { + if (in_array($dashboardPreset->getIdentifier(), $dashboardsToCreate, true)) { + $dashboard = $this->dashboardRepository->create( + $dashboardPreset, + (int)$user->user['uid'] + ); + + if ($dashboard === null) { + continue; + } + $dashboardsForUser[$dashboard->getIdentifier()] = $dashboard; + } + } + + return $dashboardsForUser; + } + + /** + * @return Dashboard[] + */ + public function getDashboardsForUser(BackendUserAuthentication $user): array + { + $dashboards = []; + foreach ($this->dashboardRepository->getDashboardsForUser((int)$user->user['uid']) as $dashboard) { + $dashboards[$dashboard->getIdentifier()] = $dashboard; + } + + if ($dashboards === []) { + $dashboards = $this->createDefaultDashboards($user); + } + + return $dashboards; + } + + protected function defineResourcesOfWidgets(): void + { + foreach ($this->widgetRegistry->getAvailableWidgets() as $widget) { + $concreteInstance = GeneralUtility::makeInstance($widget->getServiceName()); + if ($concreteInstance instanceof JavaScriptInterface) { + $this->defineJavaScriptInstructions($concreteInstance); + } + if ($concreteInstance instanceof AdditionalCssInterface) { + $this->defineCssFiles($concreteInstance); + } + if ($concreteInstance instanceof AdditionalJavaScriptInterface) { + $this->defineJsFiles($concreteInstance); + } + } + } + + protected function defineJavaScriptInstructions(JavaScriptInterface $widgetInstance): void + { + foreach ($widgetInstance->getJavaScriptModuleInstructions() as $instruction) { + $this->javaScriptModuleInstructions[] = $instruction; + } + } + + /** + * Define the correct path of the JS files of a widget and add them to the list of JS files that needs to be + * included + */ + protected function defineJsFiles(AdditionalJavaScriptInterface $widgetInstance): void + { + foreach ($widgetInstance->getJsFiles() as $jsFile) { + $this->jsFiles[$jsFile] = $jsFile; + } + } + + /** + * Define the correct path of the CSS files of a widget and add them to the list of CSS files that needs to be + * included + */ + protected function defineCssFiles(AdditionalCssInterface $widgetInstance): void + { + foreach ($widgetInstance->getCssFiles() as $cssFile) { + $this->cssFiles[$cssFile] = $cssFile; + } + } + + protected function loadCurrentDashboard(BackendUserAuthentication $user): string + { + return $user->getModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER) ?? ''; + } + + protected function saveCurrentDashboard(BackendUserAuthentication $user, string $identifier): void + { + $user->pushModuleData(self::MODULE_DATA_CURRENT_DASHBOARD_IDENTIFIER, $identifier); + } + + /** + * @return list + */ + public function getJavaScriptModuleInstructions(): array + { + return $this->javaScriptModuleInstructions; + } + + public function getJsFiles(): array + { + return $this->jsFiles; + } + + public function getCssFiles(): array + { + return $this->cssFiles; + } +} diff --git a/Classes/DashboardPreset.php b/Classes/DashboardPreset.php new file mode 100644 index 0000000..7825a12 --- /dev/null +++ b/Classes/DashboardPreset.php @@ -0,0 +1,88 @@ +}> $defaultWidgets + */ + public function __construct( + protected string $identifier, + protected string $title, + protected string $description, + protected string $iconIdentifier = 'content-dashboard', + protected array $defaultWidgets = [], + protected bool $showInWizard = true + ) {} + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getIconIdentifier(): string + { + return $this->iconIdentifier; + } + + public function getTitle(): string + { + return $this->getLanguageService()->sL($this->title) ?: $this->title; + } + + public function getDescription(): string + { + return $this->getLanguageService()->sL($this->description) ?: $this->description; + } + + /** + * @return list}> + */ + public function getDefaultWidgets(): array + { + return $this->defaultWidgets; + } + + public function isShowInWizard(): bool + { + return $this->showInWizard; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + + public function jsonSerialize(): array + { + return [ + 'identifier' => $this->getIdentifier(), + 'title' => $this->getTitle(), + 'description' => $this->getDescription(), + 'icon' => $this->getIconIdentifier(), + 'widgets' => $this->getDefaultWidgets(), + 'showInWizard' => $this->isShowInWizard(), + ]; + } +} diff --git a/Classes/DashboardPresetRegistry.php b/Classes/DashboardPresetRegistry.php new file mode 100644 index 0000000..fce79ca --- /dev/null +++ b/Classes/DashboardPresetRegistry.php @@ -0,0 +1,54 @@ +dashboardPresets)) { + $fallbackDashboardPreset = new DashboardPreset( + 'dashboardPreset-fallback', + 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default', + '', + 'content-dashboard', + [], + false + ); + + return [ + 'dashboardPreset-fallback' => $fallbackDashboardPreset, + ]; + } + + return $this->dashboardPresets; + } + + public function registerDashboardPreset(DashboardPreset $dashboardPreset): void + { + $this->dashboardPresets[$dashboardPreset->getIdentifier()] = $dashboardPreset; + } +} diff --git a/Classes/DependencyInjection/DashboardWidgetPass.php b/Classes/DependencyInjection/DashboardWidgetPass.php new file mode 100644 index 0000000..2b76627 --- /dev/null +++ b/Classes/DependencyInjection/DashboardWidgetPass.php @@ -0,0 +1,109 @@ +hasDefinition(WidgetRegistry::class)) { + return; + } + $widgetRegistryDefinition = $container->findDefinition(WidgetRegistry::class); + + foreach ($container->findTaggedServiceIds($this->tagName) as $serviceName => $tags) { + $definition = $container->findDefinition($serviceName); + $definition->setPublic(true); + + foreach ($tags as $attributes) { + $identifier = $attributes['identifier'] ?? $serviceName; + $attributes['identifier'] = $identifier; + $attributes['serviceName'] = $serviceName; + $attributes['adminOnly'] = is_a($definition->getClass(), AdminOnlyWidgetInterface::class, true); + $attributes = $this->convertAttributes($attributes); + + $configurationServiceName = $this->registerWidgetConfigurationService( + $container, + $identifier, + $attributes + ); + $definition->setArgument('$configuration', new Reference($configurationServiceName)); + + $widgetRegistryDefinition->addMethodCall('registerWidget', [$identifier . 'WidgetConfiguration']); + } + } + } + + private function convertAttributes(array $attributes): array + { + $attributes = array_merge([ + 'iconIdentifier' => 'content-dashboard', + 'height' => 'small', + 'width' => 'small', + ], $attributes); + + if (isset($attributes['groupNames'])) { + $attributes['groupNames'] = GeneralUtility::trimExplode(',', $attributes['groupNames'], true); + } else { + $attributes['groupNames'] = []; + } + + return $attributes; + } + + private function registerWidgetConfigurationService( + ContainerBuilder $container, + string $widgetIdentifier, + array $arguments + ): string { + $serviceName = $widgetIdentifier . 'WidgetConfiguration'; + + $definition = new Definition( + WidgetConfiguration::class, + $this->adjustArgumentsForDi($arguments) + ); + $definition->setPublic(true); + $container->addDefinitions([$serviceName => $definition]); + + return $serviceName; + } + + private function adjustArgumentsForDi(array $arguments): array + { + foreach ($arguments as $key => $value) { + $arguments['$' . $key] = $value; + unset($arguments[$key]); + } + + return $arguments; + } +} diff --git a/Classes/Dto/Dashboard.php b/Classes/Dto/Dashboard.php new file mode 100644 index 0000000..97b081b --- /dev/null +++ b/Classes/Dto/Dashboard.php @@ -0,0 +1,41 @@ + $this->identifier, + 'title' => $this->title, + 'widgets' => $this->widgets, + 'widgetPositions' => $this->widgetPositions, + ]; + } +} diff --git a/Classes/Dto/WidgetConfiguration.php b/Classes/Dto/WidgetConfiguration.php new file mode 100644 index 0000000..70f2f2c --- /dev/null +++ b/Classes/Dto/WidgetConfiguration.php @@ -0,0 +1,41 @@ + $this->identifier, + 'type' => $this->type, + 'height' => $this->height, + 'width' => $this->width, + ]; + } +} diff --git a/Classes/Dto/WidgetData.php b/Classes/Dto/WidgetData.php new file mode 100644 index 0000000..970bd0c --- /dev/null +++ b/Classes/Dto/WidgetData.php @@ -0,0 +1,51 @@ + $this->identifier, + 'type' => $this->type, + 'height' => $this->height, + 'width' => $this->width, + 'label' => $this->label, + 'content' => $this->content, + 'eventdata' => $this->eventdata, + 'refreshable' => $this->refreshable, + 'configurable' => $this->configurable, + ]; + } +} diff --git a/Classes/Dto/WidgetPosition.php b/Classes/Dto/WidgetPosition.php new file mode 100644 index 0000000..ecacac5 --- /dev/null +++ b/Classes/Dto/WidgetPosition.php @@ -0,0 +1,43 @@ + $this->identifier, + 'height' => $this->height, + 'width' => $this->width, + 'y' => $this->y, + 'x' => $this->x, + ]; + } +} diff --git a/Classes/EventListener/AfterBackendPageRenderEventListener.php b/Classes/EventListener/AfterBackendPageRenderEventListener.php new file mode 100644 index 0000000..2c47543 --- /dev/null +++ b/Classes/EventListener/AfterBackendPageRenderEventListener.php @@ -0,0 +1,36 @@ +pageRenderer->addCssFile('EXT:dashboard/Resources/Public/Css/Modal/style.css'); + } +} diff --git a/Classes/Exception/InvalidRssFeedException.php b/Classes/Exception/InvalidRssFeedException.php new file mode 100644 index 0000000..d0fd20a --- /dev/null +++ b/Classes/Exception/InvalidRssFeedException.php @@ -0,0 +1,22 @@ +settingDefinitionValidation->validate($definition); + if (!$omitDefaults) { + $defaultSettings[$definition->key] = $definition->default; + } + } + return $this->settingsFactory->resolveSettings( + new SettingsProvider($name, $defaultSettings, $definitions), + new SettingsProvider($name . ':instance', $settings, []), + ); + } + + public function createSettingsFromFormData(array $settings, array $definitions): SettingsInterface + { + return $this->settingsFactory->createSettingsFromFormData($settings, $definitions); + } +} diff --git a/Classes/Persistence/DashboardCreationEnricher.php b/Classes/Persistence/DashboardCreationEnricher.php new file mode 100644 index 0000000..b63bf3f --- /dev/null +++ b/Classes/Persistence/DashboardCreationEnricher.php @@ -0,0 +1,50 @@ +BE_USER->user['uid'] ?? 0; + } +} diff --git a/Classes/Repository/DashboardRepository.php b/Classes/Repository/DashboardRepository.php new file mode 100644 index 0000000..613c892 --- /dev/null +++ b/Classes/Repository/DashboardRepository.php @@ -0,0 +1,195 @@ + + */ + protected array $widgets = []; + + public function __construct( + protected readonly ConnectionPool $connectionPool, + protected readonly WidgetRegistry $widgetRegistry, + protected readonly WidgetSettingsFactory $widgetSettingsFactory, + protected readonly ContainerInterface $container + ) {} + + /** + * @return Dashboard[] + */ + public function getDashboardsForUser(int $userId): array + { + $queryBuilder = $this->getQueryBuilder(); + $rows = $queryBuilder + ->select('*') + ->from(self::TABLE) + ->where( + $queryBuilder->expr()->eq('cruser_id', $queryBuilder->createNamedParameter($userId)) + ) + ->executeQuery() + ->fetchAllAssociative(); + $results = []; + foreach ($rows as $row) { + $results[] = $this->createFromRow($row); + } + return $results; + } + + public function create(DashboardPreset $dashboardPreset, int $userId, string $title = ''): ?Dashboard + { + $widgets = []; + $title = $title ?: $dashboardPreset->getTitle(); + + foreach ($dashboardPreset->getDefaultWidgets() as $defaultWidget) { + $hash = sha1(StringUtility::getUniqueId('widget_') . '-' . $defaultWidget['identifier']); + $widgets[$hash] = $defaultWidget; + } + $identifier = sha1($dashboardPreset->getIdentifier() . '-' . time()); + $this->getQueryBuilder() + ->insert(self::TABLE) + ->values([ + 'identifier' => $identifier, + 'title' => $title, + 'tstamp' => time(), + 'crdate' => time(), + 'cruser_id' => $userId, + 'widgets' => json_encode($widgets), + ]) + ->executeStatement(); + return $this->getDashboardByIdentifier($identifier); + } + + /** + * @return int|null + */ + public function updateDashboardSettings(string $identifier, array $values) + { + $checkedValues = $this->checkAllowedFields($values); + + if (empty($checkedValues)) { + return null; + } + + $queryBuilder = $this->getQueryBuilder(); + $queryBuilder->update(self::TABLE) + ->where( + $queryBuilder->expr()->eq( + 'identifier', + $queryBuilder->createNamedParameter($identifier) + ) + ); + + foreach ($checkedValues as $field => $value) { + $queryBuilder->set($field, $value); + } + + return $queryBuilder->executeStatement(); + } + + protected function checkAllowedFields(array $values): array + { + $allowedFields = []; + foreach ($values as $field => $value) { + if (!empty($value) && in_array((string)$field, $this->allowedFields, true)) { + $allowedFields[$field] = $value; + } + } + + return $allowedFields; + } + + public function getDashboardByIdentifier(string $identifier): ?Dashboard + { + $queryBuilder = $this->getQueryBuilder(); + $row = $queryBuilder + ->select('*') + ->from(self::TABLE) + ->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($identifier))) + ->executeQuery() + ->fetchAllAssociative(); + if (count($row)) { + return $this->createFromRow($row[0]); + } + return null; + } + + /** + * @param array> $widgets + */ + public function updateWidgetConfig(Dashboard $dashboard, array $widgets): void + { + $queryBuilder = $this->getQueryBuilder(); + $queryBuilder + ->update(self::TABLE) + ->set('widgets', json_encode($widgets)) + ->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($dashboard->getIdentifier()))) + ->executeStatement(); + } + + public function delete(Dashboard $dashboard): void + { + $queryBuilder = $this->getQueryBuilder(); + $queryBuilder + ->update(self::TABLE) + ->set('deleted', 1) + ->where($queryBuilder->expr()->eq('identifier', $queryBuilder->createNamedParameter($dashboard->getIdentifier()))) + ->executeStatement(); + } + + protected function createFromRow(array $row): Dashboard + { + return GeneralUtility::makeInstance( + Dashboard::class, + $row['identifier'] ?? '', + $row['title'] ?? '', + json_decode((string)($row['widgets'] ?? ''), true) ?? [], + $this->widgetRegistry, + $this->widgetSettingsFactory, + $this->container + ); + } + + protected function getQueryBuilder(): QueryBuilder + { + return $this->connectionPool->getQueryBuilderForTable(self::TABLE); + } +} diff --git a/Classes/ServiceProvider.php b/Classes/ServiceProvider.php new file mode 100644 index 0000000..aa6b665 --- /dev/null +++ b/Classes/ServiceProvider.php @@ -0,0 +1,231 @@ + self::getDashboardPresets(...), + 'dashboard.widgetGroups' => self::getWidgetGroups(...), + 'dashboard.widgets' => self::getWidgets(...), + 'dashboard.configuration.warmer' => self::getConfigurationWarmer(...), + ]; + } + + public function getExtensions(): array + { + return [ + DashboardPresetRegistry::class => self::configureDashboardPresetRegistry(...), + ListenerProvider::class => self::addEventListeners(...), + WidgetGroupRegistry::class => self::configureWidgetGroupRegistry(...), + 'dashboard.presets' => self::configureDashboardPresets(...), + 'dashboard.widgetGroups' => self::configureWidgetGroups(...), + 'dashboard.widgets' => self::configureWidgets(...), + ] + parent::getExtensions(); + } + + public static function getDashboardPresets(ContainerInterface $container): \ArrayObject + { + return new \ArrayObject(); + } + + public static function getWidgetGroups(ContainerInterface $container): \ArrayObject + { + return new \ArrayObject(); + } + + public static function getWidgets(ContainerInterface $container): \ArrayObject + { + return new \ArrayObject(); + } + + public static function configureDashboardPresetRegistry( + ContainerInterface $container, + ?DashboardPresetRegistry $dashboardPresetRegistry = null + ): DashboardPresetRegistry { + $dashboardPresetRegistry = $dashboardPresetRegistry ?? self::new($container, DashboardPresetRegistry::class); + $cache = $container->get('cache.core'); + + $cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class)->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'Presets')->toString(); + if (!$dashboardPresetsFromPackages = $cache->require($cacheIdentifier)) { + $dashboardPresetsFromPackages = $container->get('dashboard.presets')->getArrayCopy(); + $cache->set($cacheIdentifier, 'return ' . var_export($dashboardPresetsFromPackages, true) . ';'); + } + + foreach ($dashboardPresetsFromPackages as $identifier => $options) { + // Compatibility layer for presets using the widget identifier only + $defaultWidgets = array_map( + static fn(string|array $defaultWidget): array => is_string($defaultWidget) ? ['identifier' => $defaultWidget] : $defaultWidget, + $options['defaultWidgets'] ?? [] + ); + + $preset = new DashboardPreset( + $identifier, + $options['title'], + $options['description'], + $options['iconIdentifier'], + $defaultWidgets, + $options['showInWizard'] + ); + $dashboardPresetRegistry->registerDashboardPreset($preset); + } + + return $dashboardPresetRegistry; + } + + public static function configureWidgetGroupRegistry( + ContainerInterface $container, + ?WidgetGroupRegistry $widgetGroupRegistry = null + ): WidgetGroupRegistry { + $widgetGroupRegistry = $widgetGroupRegistry ?? self::new($container, WidgetGroupRegistry::class); + $cache = $container->get('cache.core'); + + $cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class)->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'WidgetGroups')->toString(); + if (!$widgetGroupsFromPackages = $cache->require($cacheIdentifier)) { + $widgetGroupsFromPackages = $container->get('dashboard.widgetGroups')->getArrayCopy(); + $cache->set($cacheIdentifier, 'return ' . var_export($widgetGroupsFromPackages, true) . ';'); + } + + foreach ($widgetGroupsFromPackages as $identifier => $options) { + $group = new WidgetGroup( + $identifier, + $options['title'] + ); + $widgetGroupRegistry->registerWidgetGroup($group); + } + + return $widgetGroupRegistry; + } + + public static function configureDashboardPresets(ContainerInterface $container, \ArrayObject $presets): \ArrayObject + { + $paths = self::getPathsOfInstalledPackages(); + + foreach ($paths as $pathOfPackage) { + $dashboardPresetsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardPresets.php'; + if (file_exists($dashboardPresetsFileNameForPackage)) { + $definedPresetsInPackage = self::requireFile($dashboardPresetsFileNameForPackage); + if (is_array($definedPresetsInPackage)) { + $presets->exchangeArray(array_merge($presets->getArrayCopy(), $definedPresetsInPackage)); + } + } + } + + return $presets; + } + + /** + * @param string|null $path supplied when invoked internally through PseudoServiceProvider + */ + public static function configureWidgetGroups(ContainerInterface $container, \ArrayObject $widgetGroups, ?string $path = null): \ArrayObject + { + $paths = self::getPathsOfInstalledPackages(); + + foreach ($paths as $pathOfPackage) { + $widgetGroupsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardWidgetGroups.php'; + if (file_exists($widgetGroupsFileNameForPackage)) { + $definedGroupsInPackage = self::requireFile($widgetGroupsFileNameForPackage); + if (is_array($definedGroupsInPackage)) { + $widgetGroups->exchangeArray(array_merge($widgetGroups->getArrayCopy(), $definedGroupsInPackage)); + } + } + } + return $widgetGroups; + } + + /** + * @param string|null $path supplied when invoked internally through PseudoServiceProvider + */ + public static function configureWidgets(ContainerInterface $container, \ArrayObject $widgets, ?string $path = null): \ArrayObject + { + $paths = self::getPathsOfInstalledPackages(); + + foreach ($paths as $pathOfPackage) { + $widgetsFileNameForPackage = $pathOfPackage . 'Configuration/Backend/DashboardWidgets.php'; + if (file_exists($widgetsFileNameForPackage)) { + $definedWidgetsInPackage = self::requireFile($widgetsFileNameForPackage); + if (is_array($definedWidgetsInPackage)) { + $widgets->exchangeArray(array_merge($widgets->getArrayCopy(), $definedWidgetsInPackage)); + } + } + } + return $widgets; + } + + protected static function getPathsOfInstalledPackages(): array + { + $paths = []; + $packageManager = GeneralUtility::makeInstance(PackageManager::class); + + foreach ($packageManager->getActivePackages() as $package) { + $paths[] = $package->getPackagePath(); + } + + return $paths; + } + + public static function getConfigurationWarmer(ContainerInterface $container): \Closure + { + $cacheIdentifier = $container->get(PackageDependentCacheIdentifier::class); + $presetsCacheIdentifier = $cacheIdentifier->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'Presets')->toString(); + $widgetGroupsCacheIdentifier = $cacheIdentifier->withPrefix(self::CACHE_IDENTIFIER_PREFIX . 'WidgetGroups')->toString(); + return static function (CacheWarmupEvent $event) use ($container, $presetsCacheIdentifier, $widgetGroupsCacheIdentifier) { + if ($event->hasGroup('system')) { + $cache = $container->get('cache.core'); + + $dashboardPresetsFromPackages = $container->get('dashboard.presets')->getArrayCopy(); + $cache->set($presetsCacheIdentifier, 'return ' . var_export($dashboardPresetsFromPackages, true) . ';'); + + $widgetGroupsFromPackages = $container->get('dashboard.widgetGroups')->getArrayCopy(); + $cache->set($widgetGroupsCacheIdentifier, 'return ' . var_export($widgetGroupsFromPackages, true) . ';'); + } + }; + } + + public static function addEventListeners(ContainerInterface $container, ListenerProvider $listenerProvider): ListenerProvider + { + $listenerProvider->addListener(CacheWarmupEvent::class, 'dashboard.configuration.warmer'); + + return $listenerProvider; + } +} diff --git a/Classes/ViewHelpers/Widget/ButtonViewHelper.php b/Classes/ViewHelpers/Widget/ButtonViewHelper.php new file mode 100644 index 0000000..c94fad2 --- /dev/null +++ b/Classes/ViewHelpers/Widget/ButtonViewHelper.php @@ -0,0 +1,74 @@ + + * {f:translate(id: button.title, default: button.title)} + * + */ +final class ButtonViewHelper extends AbstractTagBasedViewHelper +{ + /** + * @var string + */ + protected $tagName = 'a'; + + public function initializeArguments(): void + { + parent::initializeArguments(); + $this->registerArgument('button', ButtonProviderInterface::class, 'Dashboard widget button', true); + } + + public function render(): string + { + $button = $this->arguments['button']; + + $this->tag->addAttribute('href', $button->getLink() ?: '#'); + + $target = $button->getTarget(); + if ($target !== '') { + $this->tag->addAttribute('target', $target); + if ($target === '_blank') { + $this->tag->addAttribute('rel', 'noreferrer'); + } + } + + if ($button instanceof ElementAttributesInterface) { + $this->tag->addAttributes($button->getElementAttributes()); + } + + $this->tag->setContent((string)$this->renderChildren()); + $this->tag->forceClosingTag(true); + + return $this->tag->render(); + } +} diff --git a/Classes/WidgetApi.php b/Classes/WidgetApi.php new file mode 100644 index 0000000..93e234d --- /dev/null +++ b/Classes/WidgetApi.php @@ -0,0 +1,40 @@ +identifier; + } + + public function getTitle(): string + { + return $this->getLanguageService()->sL($this->title) ?: $this->title; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/WidgetGroupInitializationService.php b/Classes/WidgetGroupInitializationService.php new file mode 100644 index 0000000..27c9727 --- /dev/null +++ b/Classes/WidgetGroupInitializationService.php @@ -0,0 +1,74 @@ +widgetGroupRegistry->getWidgetGroups() as $widgetGroup) { + $widgets = []; + $widgetGroupIdentifier = $widgetGroup->getIdentifier(); + + $widgetsForGroup = $this->widgetRegistry->getAvailableWidgetsForWidgetGroup($widgetGroupIdentifier); + foreach ($widgetsForGroup as $widgetConfiguration) { + $widgetIdentifier = $widgetConfiguration->getIdentifier(); + + $widgets[] = [ + 'identifier' => $widgetIdentifier, + 'icon' => $widgetConfiguration->getIconIdentifier(), + 'label' => $this->getLanguageService()->sL($widgetConfiguration->getTitle()), + 'description' => $this->getLanguageService()->sL($widgetConfiguration->getDescription()), + 'requestType' => 'event', + 'event' => 'typo3:dashboard:widget:add', + ]; + } + + if ($widgets === []) { + continue; + } + + $groupConfigurations[$widgetGroupIdentifier] = [ + 'identifier' => $widgetGroupIdentifier, + 'label' => $widgetGroup->getTitle(), + 'items' => $widgets, + ]; + } + + return $groupConfigurations; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/WidgetGroupRegistry.php b/Classes/WidgetGroupRegistry.php new file mode 100644 index 0000000..0a200d0 --- /dev/null +++ b/Classes/WidgetGroupRegistry.php @@ -0,0 +1,39 @@ +widgetGroups; + } + + public function registerWidgetGroup(WidgetGroup $widgetGroup): void + { + $this->widgetGroups[$widgetGroup->getIdentifier()] = $widgetGroup; + } +} diff --git a/Classes/WidgetRegistry.php b/Classes/WidgetRegistry.php new file mode 100644 index 0000000..7357891 --- /dev/null +++ b/Classes/WidgetRegistry.php @@ -0,0 +1,139 @@ + + */ + private $widgets = []; + + /** + * @var array + */ + private $widgetsPerWidgetGroup = []; + + public function __construct( + protected readonly ContainerInterface $container, + protected readonly WidgetSettingsFactory $widgetSettingsFactory, + ) {} + + /** + * @return WidgetConfigurationInterface[] + */ + public function getAvailableWidgets(): array + { + return $this->checkPermissionOfWidgets($this->widgets); + } + + /** + * @return WidgetConfigurationInterface[] + */ + public function getAllWidgets(): array + { + return $this->widgets; + } + + /** + * @throws \InvalidArgumentException If requested identifier does not exist. + */ + public function getAvailableWidget(ServerRequestInterface $request, string $identifier): WidgetRendererInterface|WidgetInterface + { + if (array_key_exists($identifier, $this->getAvailableWidgets())) { + $widget = $this->container->get($this->widgets[$identifier]->getServiceName()); + if ($widget instanceof RequestAwareWidgetInterface) { + $widget->setRequest($request); + } + return $widget; + } + throw new \InvalidArgumentException('Requested widget "' . $identifier . '" does not exist.', 1584777201); + } + + /** + * @return WidgetConfigurationInterface[] + */ + public function getAvailableWidgetsForWidgetGroup(string $widgetGroupIdentifier): array + { + if (!array_key_exists($widgetGroupIdentifier, $this->widgetsPerWidgetGroup)) { + return []; + } + return $this->checkPermissionOfWidgets($this->widgetsPerWidgetGroup[$widgetGroupIdentifier]); + } + + public function registerWidget(string $serviceName): void + { + $widgetConfiguration = $this->container->get($serviceName); + $this->widgets[$widgetConfiguration->getIdentifier()] = $widgetConfiguration; + foreach ($widgetConfiguration->getGroupNames() as $groupIdentifier) { + $this->widgetsPerWidgetGroup = ArrayUtility::setValueByPath( + $this->widgetsPerWidgetGroup, + $groupIdentifier . '/' . $widgetConfiguration->getIdentifier(), + $widgetConfiguration + ); + } + } + + /** + * @param WidgetConfigurationInterface[] $widgets + * @return WidgetConfigurationInterface[] + */ + protected function checkPermissionOfWidgets(array $widgets): array + { + return array_filter($widgets, function ($widget, $identifier) { + return $this->getBackendUser()->check('available_widgets', $identifier) + && (!$widget instanceof WidgetConfiguration || !$widget->isAdminOnly() || $this->getBackendUser()->isAdmin()); + }, ARRAY_FILTER_USE_BOTH); + } + + public function widgetItemsProcFunc(array &$parameters): void + { + foreach ($this->widgets as $widget) { + if ($widget instanceof WidgetConfiguration && $widget->isAdminOnly()) { + continue; + } + $parameters['items'][] = [ + 'label' => $widget->getTitle(), + 'value' => $widget->getIdentifier(), + 'icon' => $widget->getIconIdentifier(), + 'description' => $widget->getDescription(), + ]; + } + } + + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Widgets/AdditionalCssInterface.php b/Classes/Widgets/AdditionalCssInterface.php new file mode 100644 index 0000000..8018495 --- /dev/null +++ b/Classes/Widgets/AdditionalCssInterface.php @@ -0,0 +1,30 @@ + it will not be able to assign to a user group + */ +interface AdminOnlyWidgetInterface {} diff --git a/Classes/Widgets/BarChartWidget.php b/Classes/Widgets/BarChartWidget.php new file mode 100644 index 0000000..df44462 --- /dev/null +++ b/Classes/Widgets/BarChartWidget.php @@ -0,0 +1,110 @@ +request = $request; + } + + public function renderWidgetContent(): string + { + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'button' => $this->buttonProvider, + 'options' => $this->options, + 'configuration' => $this->configuration, + ]); + return $view->render('Widget/ChartWidget'); + } + + public function getEventData(): array + { + return [ + 'graphConfig' => [ + 'type' => 'bar', + 'options' => [ + 'maintainAspectRatio' => false, + 'plugins' => [ + 'legend' => [ + 'display' => false, + ], + ], + 'scales' => [ + 'y' => [ + 'ticks' => [ + 'beginAtZero' => true, + ], + ], + 'x' => [ + 'ticks' => [ + 'maxTicksLimit' => 15, + ], + ], + ], + ], + 'data' => $this->dataProvider->getChartData(), + ], + ]; + } + + public function getCssFiles(): array + { + return []; + } + + public function getJavaScriptModuleInstructions(): array + { + return [ + JavaScriptModuleInstruction::create('@typo3/dashboard/contrib/chartjs.js'), + JavaScriptModuleInstruction::create('@typo3/dashboard/chart-initializer.js'), + ]; + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/Classes/Widgets/BookmarksWidget.php b/Classes/Widgets/BookmarksWidget.php new file mode 100644 index 0000000..7b882a1 --- /dev/null +++ b/Classes/Widgets/BookmarksWidget.php @@ -0,0 +1,133 @@ +options['group'] ?? '', + label: 'dashboard.widget_bookmarks:widget.bookmarks.setting.groups.label', + description: 'dashboard.widget_bookmarks:widget.bookmarks.setting.groups.description', + readonly: array_key_exists('group', $this->options), + enum: $this->getAvailableGroups(), + ), + new SettingDefinition( + key: 'limit', + type: 'int', + default: (int)($this->options['limit'] ?? 0), + label: 'dashboard.widget_bookmarks:widget.bookmarks.setting.limit.label', + description: 'dashboard.widget_bookmarks:widget.bookmarks.setting.limit.description', + readonly: array_key_exists('limit', $this->options), + ), + ]; + } + + public function renderWidget(WidgetContext $context): WidgetResult + { + $view = $this->backendViewFactory->create($context->request); + $view->assignMultiple([ + 'options' => $this->options, + 'settings' => $context->settings, + 'configuration' => $this->configuration, + ]); + + return new WidgetResult( + content: $view->render('Widget/BookmarksWidget'), + label: $this->resolveLabel($context->settings), + refreshable: true, + ); + } + + public function getJavaScriptModuleInstructions(): array + { + return [ + JavaScriptModuleInstruction::create('@typo3/dashboard/widget/bookmarks-widget-element.js'), + ]; + } + + private function getAvailableGroups(): array + { + $groups = [ + '' => 'dashboard.widget_bookmarks:widget.bookmarks.setting.showAll', + ]; + + // Only show groups that contain bookmarks + $groupsWithBookmarks = []; + foreach ($this->bookmarkService->getBookmarks() as $bookmark) { + $groupsWithBookmarks[$bookmark->groupId] = true; + } + + foreach ($this->bookmarkService->getGroups() as $group) { + if (isset($groupsWithBookmarks[$group->id])) { + $groups[(string)$group->id] = $group->label; + } + } + return $groups; + } + + private function resolveLabel(SettingsInterface $settings): ?string + { + $group = $settings->get('group'); + if ($group !== '') { + $groupId = is_numeric($group) ? (int)$group : $group; + foreach ($this->bookmarkService->getGroups() as $bookmarkGroup) { + if ($bookmarkGroup->id === $groupId) { + $widgetTitle = $this->getLanguageService()->sL($this->configuration->getTitle()); + return $widgetTitle . ': ' . $bookmarkGroup->label; + } + } + } + + // Fall back to default widget title + return null; + } + + private function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Widgets/ButtonProviderInterface.php b/Classes/Widgets/ButtonProviderInterface.php new file mode 100644 index 0000000..7869442 --- /dev/null +++ b/Classes/Widgets/ButtonProviderInterface.php @@ -0,0 +1,41 @@ +options = array_merge(['text' => ''], $options); + } + + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + public function renderWidgetContent(): string + { + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'text' => $this->options['text'], + 'options' => $this->options, + 'button' => $this->buttonProvider, + 'configuration' => $this->configuration, + ]); + return $view->render('Widget/CtaWidget'); + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/Classes/Widgets/DoughnutChartWidget.php b/Classes/Widgets/DoughnutChartWidget.php new file mode 100644 index 0000000..4cb61d0 --- /dev/null +++ b/Classes/Widgets/DoughnutChartWidget.php @@ -0,0 +1,100 @@ +request = $request; + } + + public function renderWidgetContent(): string + { + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'button' => $this->buttonProvider, + 'options' => $this->options, + 'configuration' => $this->configuration, + ]); + return $view->render('Widget/ChartWidget'); + } + + public function getEventData(): array + { + return [ + 'graphConfig' => [ + 'type' => 'doughnut', + 'options' => [ + 'maintainAspectRatio' => false, + 'plugins' => [ + 'legend' => [ + 'display' => true, + 'position' => 'bottom', + ], + ], + 'cutoutPercentage' => 60, + ], + 'data' => $this->dataProvider->getChartData(), + ], + ]; + } + + public function getCssFiles(): array + { + return []; + } + + public function getJavaScriptModuleInstructions(): array + { + return [ + JavaScriptModuleInstruction::create('@typo3/dashboard/contrib/chartjs.js'), + JavaScriptModuleInstruction::create('@typo3/dashboard/chart-initializer.js'), + ]; + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/Classes/Widgets/ElementAttributesInterface.php b/Classes/Widgets/ElementAttributesInterface.php new file mode 100644 index 0000000..c09953d --- /dev/null +++ b/Classes/Widgets/ElementAttributesInterface.php @@ -0,0 +1,29 @@ + + */ + public function getElementAttributes(): array; +} diff --git a/Classes/Widgets/EventDataInterface.php b/Classes/Widgets/EventDataInterface.php new file mode 100644 index 0000000..b0395b5 --- /dev/null +++ b/Classes/Widgets/EventDataInterface.php @@ -0,0 +1,29 @@ + + */ + public function getJavaScriptModuleInstructions(): array; +} diff --git a/Classes/Widgets/LatestBeLoginsWidget.php b/Classes/Widgets/LatestBeLoginsWidget.php new file mode 100644 index 0000000..8bb5956 --- /dev/null +++ b/Classes/Widgets/LatestBeLoginsWidget.php @@ -0,0 +1,83 @@ + 1, + ], + ), + ]; + } + + public function renderWidget(WidgetContext $context): WidgetResult + { + $limit = $context->settings->get('limit'); + $items = $this->dataProvider->getItems($limit); + + $view = $this->backendViewFactory->create($context->request); + $view->assignMultiple([ + 'items' => $items, + 'button' => $this->buttonProvider, + 'configuration' => $this->configuration, + 'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], + ]); + + return new WidgetResult( + content: $view->render('Widget/LatestBeLoginsWidget'), + refreshable: true, + ); + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Widgets/LatestChangedPagesWidget.php b/Classes/Widgets/LatestChangedPagesWidget.php new file mode 100644 index 0000000..611687f --- /dev/null +++ b/Classes/Widgets/LatestChangedPagesWidget.php @@ -0,0 +1,284 @@ +options = array_merge([ + 'limit' => 10, + 'historyLimit' => 1000, + ], $options); + } + + public function getSettingsDefinitions(): array + { + return [ + new SettingDefinition( + key: 'restrictToCurrentUser', + type: 'bool', + default: false, + label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.settings.restrictToCurrentUser', + ), + ]; + } + + public function renderWidget(WidgetContext $context): WidgetResult + { + $restrictToCurrentUser = (bool)$context->settings->get('restrictToCurrentUser'); + $sysHistoryEntries = $this->getSysHistoryEntries($this->options['historyLimit'], $restrictToCurrentUser); + $latestPages = $this->getLatestPagesFromSysHistory($sysHistoryEntries, $this->options['limit']); + $latestPages = $this->enrichPageInformation($latestPages); + + $view = $this->backendViewFactory->create($context->request); + $view->assignMultiple([ + 'latestPages' => $latestPages, + 'configuration' => $this->configuration, + 'dateFormat' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], + ]); + + return new WidgetResult( + content: $view->render('Widget/LatestChangedPagesWidget'), + refreshable: true, + label: $restrictToCurrentUser ? $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.title.restrictToCurrentUser') : null, + ); + } + + private function getSysHistoryEntries(int $limit, bool $restrictToCurrentUser): array + { + $queryBuilder = $this->getQueryBuilderSysHistory(); + $workspaceId = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id'); + $queryBuilder + ->select('tablename', 'recuid', 'tstamp', 'userid') + ->from('sys_history') + ->where( + $queryBuilder->expr()->in( + 'tablename', + [ + $queryBuilder->createNamedParameter('pages'), + $queryBuilder->createNamedParameter('tt_content'), + ] + ), + $queryBuilder->expr()->eq('workspace', $workspaceId), + ) + ->addOrderBy('tstamp', 'desc') + ->setMaxResults($limit); + + if ($restrictToCurrentUser) { + $queryBuilder->andWhere( + $queryBuilder->expr()->eq( + 'userid', + $queryBuilder->createNamedParameter($this->getBackendUser()->user['uid']) + ), + ); + } + + return $queryBuilder + ->executeQuery() + ->fetchAllAssociative(); + } + + private function getLatestPagesFromSysHistory(array $history, int $limit): array + { + $latestPages = []; + foreach ($history as $historyEntry) { + $pageId = $historyEntry['recuid']; + if ($historyEntry['tablename'] === 'tt_content') { + $pageId = $this->getPageOfContentElement($historyEntry['recuid']); + } + if (!$pageId || isset($latestPages[$pageId])) { + continue; + } + + $pageRecord = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); + if ($pageRecord === false || $pageRecord === []) { + // Backend user has no access to show page information. Dismiss this page. + continue; + } + + $latestPages[$pageId]['history'] = $historyEntry; + $pageRecord['_uid'] = $pageRecord['language_tag'] > 0 ? $pageRecord['l10n_parent'] : $pageRecord['uid']; + $latestPages[$pageId]['pageRecord'] = $pageRecord; + try { + $latestPages[$pageId]['siteLanguage'] = $this->siteFinder->getSiteByPageId($pageRecord['_uid'])->getLanguageById($pageRecord['language_tag']); + } catch (SiteNotFoundException $exception) { + $latestPages[$pageId]['siteLanguage'] = null; + } + // Override tstamp of pageRecord with tstamp from history record if newer + $latestPages[$pageId]['pageRecord']['tstamp'] = max($historyEntry['tstamp'], $latestPages[$pageId]['pageRecord']['tstamp']); + + if (count($latestPages) >= $limit) { + break; + } + } + return $latestPages; + } + + private function enrichPageInformation(array $latestPages): array + { + $userNames = BackendUtility::getUserNames('username,realName,uid'); + + foreach ($latestPages as $pageId => &$page) { + $page['rootline'] = $this->getRootLine($pageId); + + $uriPageId = $page['pageRecord']['language_tag'] > 0 ? $page['pageRecord']['l10n_parent'] : $page['pageRecord']['uid']; + $page['viewLink'] = (string)PreviewUriBuilder::create($page['pageRecord']) + ->withRootLine(BackendUtility::BEgetRootLine($uriPageId)) + ->buildUri(); + $page['userName'] = $userNames[$page['history']['userid']]['username'] ?? ''; + $page['realName'] = $userNames[$page['history']['userid']]['realName'] ?? ''; + } + + return $latestPages; + } + + private function getPageOfContentElement(int $uid): ?int + { + $queryBuilder = $this->getQueryBuilderForContentElements(); + $contentRecord = $queryBuilder + ->select('pid', 'sys_language_uid') + ->from('tt_content') + ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT))) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative(); + + if (!$contentRecord) { + // Content element has been deleted + return null; + } + + $queryBuilder = $this->getQueryBuilderForPages(); + if ((int)$contentRecord['language_tag'] > 0) { + return $queryBuilder + ->select('uid') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($contentRecord['pid'], Connection::PARAM_INT)), + $queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter($contentRecord['language_tag'], Connection::PARAM_INT)) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative()['uid'] ?? null; + } + + return $queryBuilder + ->select('uid') + ->from('pages') + ->where( + $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($contentRecord['pid'], Connection::PARAM_INT)) + ) + ->setMaxResults(1) + ->executeQuery() + ->fetchAssociative()['uid'] ?? null; + } + + private function getRootLine(int $pageId): string + { + $rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId)->get(); + return implode(' / ', array_slice( + array_map( + static fn(array $page): string => $page['title'], + array_reverse($rootLine) + ), + 0, + -1 + )); + } + + private function getQueryBuilderSysHistory(): QueryBuilder + { + $queryBuilder = $this->connectionPool->getConnectionForTable('sys_history')->createQueryBuilder(); + return $queryBuilder; + } + + private function getQueryBuilderForContentElements(): QueryBuilder + { + $queryBuilder = $this->connectionPool->getConnectionForTable('tt_content')->createQueryBuilder(); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + return $queryBuilder; + } + + private function getQueryBuilderForPages(): QueryBuilder + { + $queryBuilder = $this->connectionPool->getConnectionForTable('pages')->createQueryBuilder(); + $queryBuilder->getRestrictions()->removeAll(); + $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + return $queryBuilder; + } + + public function getOptions(): array + { + return $this->options; + } + + private function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Widgets/ListDataProviderInterface.php b/Classes/Widgets/ListDataProviderInterface.php new file mode 100644 index 0000000..634a209 --- /dev/null +++ b/Classes/Widgets/ListDataProviderInterface.php @@ -0,0 +1,30 @@ +request = $request; + } + + public function renderWidgetContent(): string + { + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'items' => $this->getItems(), + 'options' => $this->options, + 'button' => $this->buttonProvider, + 'configuration' => $this->configuration, + ]); + return $view->render('Widget/ListWidget'); + } + + protected function getItems(): array + { + return $this->dataProvider->getItems(); + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/Classes/Widgets/NumberWithIconDataProviderInterface.php b/Classes/Widgets/NumberWithIconDataProviderInterface.php new file mode 100644 index 0000000..73c4b19 --- /dev/null +++ b/Classes/Widgets/NumberWithIconDataProviderInterface.php @@ -0,0 +1,29 @@ +request = $request; + } + + public function renderWidgetContent(): string + { + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'icon' => $this->options['icon'] ?? '', + 'title' => $this->options['title'] ?? '', + 'subtitle' => $this->options['subtitle'] ?? '', + 'number' => $this->dataProvider->getNumber(), + 'options' => $this->options, + 'configuration' => $this->configuration, + ]); + return $view->render('Widget/NumberWithIconWidget'); + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/Classes/Widgets/Provider/ButtonProvider.php b/Classes/Widgets/Provider/ButtonProvider.php new file mode 100644 index 0000000..2231a90 --- /dev/null +++ b/Classes/Widgets/Provider/ButtonProvider.php @@ -0,0 +1,47 @@ +title; + } + + public function getLink(): string + { + return $this->link; + } + + public function getTarget(): string + { + return $this->target; + } +} diff --git a/Classes/Widgets/Provider/LatestBeLoginsDataProvider.php b/Classes/Widgets/Provider/LatestBeLoginsDataProvider.php new file mode 100644 index 0000000..c2fb909 --- /dev/null +++ b/Classes/Widgets/Provider/LatestBeLoginsDataProvider.php @@ -0,0 +1,78 @@ +connectionPool->getQueryBuilderForTable('sys_log'); + + $logEntries = $queryBuilder + ->select('uid', 'tstamp', 'userid', 'details') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'action', + $queryBuilder->createNamedParameter(SystemLogLoginAction::LOGIN, Connection::PARAM_INT) + ) + ) + ->orderBy('tstamp', 'DESC') + ->setMaxResults($limit) + ->executeQuery() + ->fetchAllAssociative(); + + // Enrich with user information + $items = []; + $userNames = BackendUtility::getUserNames('username,realName,uid'); + + foreach ($logEntries as $entry) { + $userId = (int)$entry['userid']; + $userInfo = $userNames[$userId] ?? null; + if ($userInfo !== null) { + $items[] = [ + 'timestamp' => (int)$entry['tstamp'], + 'userId' => $userId, + 'username' => $userInfo['username'] ?? '', + 'realName' => $userInfo['realName'] ?? '', + 'details' => $entry['details'], + ]; + } + } + + return $items; + } +} diff --git a/Classes/Widgets/Provider/NumberOfFailedLoginsDataProvider.php b/Classes/Widgets/Provider/NumberOfFailedLoginsDataProvider.php new file mode 100644 index 0000000..86012fd --- /dev/null +++ b/Classes/Widgets/Provider/NumberOfFailedLoginsDataProvider.php @@ -0,0 +1,58 @@ +getQueryBuilderForTable('sys_log'); + + return (int)$queryBuilder->count('uid') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::LOGIN, Connection::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'action', + $queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, Connection::PARAM_INT) + ), + $queryBuilder->expr()->neq( + 'error', + $queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gt( + 'tstamp', + $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'] - $secondsBack, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + } +} diff --git a/Classes/Widgets/Provider/SysLogButtonProvider.php b/Classes/Widgets/Provider/SysLogButtonProvider.php new file mode 100644 index 0000000..49cf33d --- /dev/null +++ b/Classes/Widgets/Provider/SysLogButtonProvider.php @@ -0,0 +1,63 @@ +title; + } + + public function getLink(): string + { + return ''; + } + + public function getTarget(): string + { + return $this->target; + } + + public function getElementAttributes(): array + { + if (!ExtensionManagementUtility::isLoaded('belog')) { + return []; + } + return [ + 'data-dispatch-action' => 'TYPO3.ModuleMenu.showModule', + 'data-dispatch-args-list' => 'system_log,&' + . http_build_query(['constraint' => ['channel' => $this->channel ?: 'php']]), + ]; + } +} diff --git a/Classes/Widgets/Provider/SysLogErrorsDataProvider.php b/Classes/Widgets/Provider/SysLogErrorsDataProvider.php new file mode 100644 index 0000000..0603932 --- /dev/null +++ b/Classes/Widgets/Provider/SysLogErrorsDataProvider.php @@ -0,0 +1,99 @@ +calculateDataForLastDays(); + + return [ + 'labels' => $this->labels, + 'datasets' => [ + [ + 'label' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.chart.dataSet.0'), + 'backgroundColor' => WidgetApi::getDefaultChartColors()[0], + 'border' => 0, + 'data' => $this->data, + ], + ], + ]; + } + + protected function getNumberOfErrorsInPeriod(int $start, int $end): int + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log'); + return (int)$queryBuilder + ->count('*') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::ERROR, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gte( + 'tstamp', + $queryBuilder->createNamedParameter($start, Connection::PARAM_INT) + ), + $queryBuilder->expr()->lte( + 'tstamp', + $queryBuilder->createNamedParameter($end, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + } + + protected function calculateDataForLastDays(): void + { + $format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d'; + + for ($daysBefore = $this->days; $daysBefore >= 0; $daysBefore--) { + $this->labels[] = date($format, (int)strtotime('-' . $daysBefore . ' day')); + $startPeriod = (int)strtotime('-' . $daysBefore . ' day 0:00:00'); + $endPeriod = (int)strtotime('-' . $daysBefore . ' day 23:59:59'); + + $this->data[] = $this->getNumberOfErrorsInPeriod($startPeriod, $endPeriod); + } + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Widgets/Provider/TypeOfUsersChartDataProvider.php b/Classes/Widgets/Provider/TypeOfUsersChartDataProvider.php new file mode 100644 index 0000000..7e42cee --- /dev/null +++ b/Classes/Widgets/Provider/TypeOfUsersChartDataProvider.php @@ -0,0 +1,66 @@ +languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']); + $adminUsers = $this->getNumberOfUsers(true); + $normalUsers = $this->getNumberOfUsers(false); + + return [ + 'labels' => [ + $languageService->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.normalUsers'), + $languageService->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.adminUsers'), + ], + 'datasets' => [ + [ + 'backgroundColor' => WidgetApi::getDefaultChartColors(), + 'data' => [$normalUsers, $adminUsers], + ], + ], + ]; + } + + protected function getNumberOfUsers(bool $admin = false): int + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users'); + return (int)$queryBuilder + ->count('*') + ->from('be_users') + ->where( + $queryBuilder->expr()->eq( + 'admin', + $queryBuilder->createNamedParameter($admin ? 1 : 0, Connection::PARAM_INT) + ) + ) + ->executeQuery() + ->fetchOne(); + } +} diff --git a/Classes/Widgets/RequestAwareWidgetInterface.php b/Classes/Widgets/RequestAwareWidgetInterface.php new file mode 100644 index 0000000..8ea60c2 --- /dev/null +++ b/Classes/Widgets/RequestAwareWidgetInterface.php @@ -0,0 +1,31 @@ +options), + ), + new SettingDefinition( + key: 'feedUrl', + type: 'url', + default: (string)($this->options['feedUrl'] ?? ''), + label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.label', + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.fieldUrl.description', + readonly: array_key_exists('feedUrl', $this->options), + options: [ + 'pattern' => 'https?://.+', + ], + ), + new SettingDefinition( + key: 'limit', + type: 'int', + default: (int)($this->options['limit'] ?? 5), + label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.label', + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.limit.description', + readonly: array_key_exists('limit', $this->options), + ), + new SettingDefinition( + key: 'lifeTime', + type: 'int', + default: (int)($this->options['lifeTime'] ?? 43200), + label: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.label', + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.setting.lifeTime.description', + readonly: true, + ), + ]; + } + + public function renderWidget(WidgetContext $context): WidgetResult + { + $view = $this->backendViewFactory->create($context->request); + $feedUrl = $context->settings->get('feedUrl'); + $items = []; + if ($feedUrl) { + try { + $items = $this->getFeedItems($context->settings); + } catch (InvalidRssFeedException) { + $view->assign('invalidFeed', true); + } + } + $view->assignMultiple([ + 'feedUrl' => $feedUrl, + 'items' => $items, + 'settings' => $context->settings, + 'options' => $this->options, + 'button' => $this->buttonProvider, + 'configuration' => $this->configuration, + ]); + return new WidgetResult( + label: $context->settings->get('label') !== '' ? $context->settings->get('label') : null, + content: $view->render('Widget/RssWidget'), + refreshable: true, + ); + } + + protected function getFeedItems(SettingsInterface $settings): array + { + $cacheHash = md5($settings->get('feedUrl') . '-' . $settings->get('limit')); + if ($items = $this->cache->get($cacheHash)) { + return $items; + } + + $feedContent = GeneralUtility::getUrl($settings->get('feedUrl')); + if ($feedContent === false) { + throw new InvalidRssFeedException('RSS URL could not be fetched', 1573385431); + } + try { + $feedXml = simplexml_load_string($feedContent); + } catch (\Exception $e) { + throw new InvalidRssFeedException('Received RSS feed could not be parsed.', 1573385432, $e); + } + + $items = match ($this->determineFeedType($feedXml)) { + 'atom' => $this->parseAtomFeed($feedXml), + 'rss' => $this->parseRssFeed($feedXml), + default => [] + }; + + usort($items, static function ($item1, $item2) { + return new \DateTime($item2['pubDate']) <=> new \DateTime($item1['pubDate']); + }); + $items = array_slice($items, 0, (int)$settings->get('limit')); + + $this->cache->set($cacheHash, $items, ['dashboard_rss'], (int)$settings->get('lifeTime')); + + return $items; + } + + protected function determineFeedType(\SimpleXMLElement $feedXml): string + { + return $feedXml->getName() === 'feed' ? 'atom' : 'rss'; + } + + protected function parseRssFeed(\SimpleXMLElement $rssFeed): array + { + $items = []; + foreach ($rssFeed->channel->item as $item) { + $items[] = [ + 'title' => trim((string)$item->title), + 'link' => trim((string)$item->link), + 'pubDate' => trim((string)$item->pubDate), + 'description' => trim(strip_tags((string)$item->description)), + ]; + } + return $items; + } + + protected function parseAtomFeed(\SimpleXMLElement $atomFeed): array + { + $items = []; + foreach ($atomFeed->entry as $entry) { + $items[] = [ + 'title' => trim((string)$entry->title), + 'link' => trim((string)($entry->link['href'] ?? '')), + 'pubDate' => trim((string)($entry->published ?? $entry->updated ?? '')), + 'description' => trim(strip_tags((string)($entry->summary ?? $entry->content ?? ''))), + 'author' => [ + 'name' => trim((string)($entry->author->name ?? '')), + 'email' => trim((string)($entry->author->email ?? '')), + 'url' => trim((string)($entry->author->url ?? '')), + ], + ]; + } + return $items; + } +} diff --git a/Classes/Widgets/T3GeneralInformationWidget.php b/Classes/Widgets/T3GeneralInformationWidget.php new file mode 100644 index 0000000..42d146c --- /dev/null +++ b/Classes/Widgets/T3GeneralInformationWidget.php @@ -0,0 +1,68 @@ +request = $request; + } + + public function renderWidgetContent(): string + { + $typo3Information = new Typo3Information(); + $typo3Version = new Typo3Version(); + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'title' => 'TYPO3 CMS ' . $typo3Version->getVersion(), + 'copyrightYear' => $typo3Information->getCopyrightYear(), + 'currentVersion' => $typo3Version->getVersion(), + 'donationUrl' => $typo3Information::URL_DONATE, + 'copyRightNotice' => $typo3Information->getCopyrightNotice(), + 'options' => $this->options, + 'configuration' => $this->configuration, + ]); + return $view->render('Widget/T3GeneralInformationWidget'); + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/Classes/Widgets/WidgetConfiguration.php b/Classes/Widgets/WidgetConfiguration.php new file mode 100644 index 0000000..78aaacc --- /dev/null +++ b/Classes/Widgets/WidgetConfiguration.php @@ -0,0 +1,89 @@ +identifier; + } + + public function getServiceName(): string + { + return $this->serviceName; + } + + public function getGroupNames(): array + { + return $this->groupNames; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getIconIdentifier(): string + { + return $this->iconIdentifier; + } + + public function getHeight(): string + { + return $this->height; + } + + public function getWidth(): string + { + return $this->width; + } + + public function isAdminOnly(): bool + { + return $this->adminOnly; + } +} diff --git a/Classes/Widgets/WidgetConfigurationInterface.php b/Classes/Widgets/WidgetConfigurationInterface.php new file mode 100644 index 0000000..632d350 --- /dev/null +++ b/Classes/Widgets/WidgetConfigurationInterface.php @@ -0,0 +1,66 @@ + [ + 'path' => '/dashboard/dashboards/get', + 'target' => DashboardAjaxController::class . '::getDashboards', + 'methods' => ['GET'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_dashboard_add' => [ + 'path' => '/dashboard/dashboard/add', + 'target' => DashboardAjaxController::class . '::addDashboard', + 'methods' => ['POST'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_dashboard_edit' => [ + 'path' => '/dashboard/dashboard/edit', + 'target' => DashboardAjaxController::class . '::editDashboard', + 'methods' => ['POST'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_dashboard_update' => [ + 'path' => '/dashboard/dashboard/update', + 'target' => DashboardAjaxController::class . '::updateDashboard', + 'methods' => ['POST'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_dashboard_delete' => [ + 'path' => '/dashboard/dashboard/delete', + 'target' => DashboardAjaxController::class . '::deleteDashboard', + 'methods' => ['POST'], + 'inheritAccessFromModule' => 'dashboard', + ], + + // Presets + 'dashboard_presets_get' => [ + 'path' => '/dashboard/presets/get', + 'target' => DashboardAjaxController::class . '::getPresets', + 'methods' => ['GET'], + 'inheritAccessFromModule' => 'dashboard', + ], + + // Categories + 'dashboard_categories_get' => [ + 'path' => '/dashboard/categories/get', + 'target' => DashboardAjaxController::class . '::getCategories', + 'methods' => ['GET'], + 'inheritAccessFromModule' => 'dashboard', + ], + + // Widgets + 'dashboard_widget_get' => [ + 'path' => '/dashboard/widget/get', + 'target' => DashboardAjaxController::class . '::getWidget', + 'methods' => ['GET'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_widget_add' => [ + 'path' => '/dashboard/widget/add', + 'target' => DashboardAjaxController::class . '::addWidget', + 'methods' => ['POST'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_widget_remove' => [ + 'path' => '/dashboard/widget/remove', + 'target' => DashboardAjaxController::class . '::removeWidget', + 'methods' => ['POST'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_widget_settings_get' => [ + 'path' => '/dashboard/widget/settings/get', + 'target' => DashboardAjaxController::class . '::getWidgetSettings', + 'methods' => ['GET'], + 'inheritAccessFromModule' => 'dashboard', + ], + 'dashboard_widget_settings_update' => [ + 'path' => '/dashboard/widget/settings/update', + 'target' => DashboardAjaxController::class . '::updateWidgetSettings', + 'methods' => ['POST'], + 'inheritAccessFromModule' => 'dashboard', + ], +]; diff --git a/Configuration/Backend/DashboardPresets.php b/Configuration/Backend/DashboardPresets.php new file mode 100644 index 0000000..b4ae0e7 --- /dev/null +++ b/Configuration/Backend/DashboardPresets.php @@ -0,0 +1,18 @@ + [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default', + 'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default.description', + 'iconIdentifier' => 'content-dashboard', + 'defaultWidgets' => ['t3information', 'docGettingStarted'], + 'showInWizard' => false, + ], + 'empty' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.empty', + 'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.empty.description', + 'iconIdentifier' => 'content-dashboard-empty', + 'defaultWidgets' => [], + 'showInWizard' => true, + ], +]; diff --git a/Configuration/Backend/DashboardWidgetGroups.php b/Configuration/Backend/DashboardWidgetGroups.php new file mode 100644 index 0000000..eac6e39 --- /dev/null +++ b/Configuration/Backend/DashboardWidgetGroups.php @@ -0,0 +1,24 @@ + [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.general', + ], + 'systemInfo' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.system', + ], + 'typo3' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.typo3', + ], + 'news' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.news', + ], + 'documentation' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.documentation', + ], + 'content' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.content', + ], +]; diff --git a/Configuration/Backend/Modules.php b/Configuration/Backend/Modules.php new file mode 100644 index 0000000..cc922aa --- /dev/null +++ b/Configuration/Backend/Modules.php @@ -0,0 +1,22 @@ + [ + 'position' => ['before' => '*'], + 'standalone' => true, + 'access' => 'user', + 'path' => '/module/dashboard', + 'iconIdentifier' => 'module-dashboard', + 'labels' => 'dashboard.module', + 'routes' => [ + '_default' => [ + 'target' => DashboardController::class . '::mainAction', + ], + ], + ], +]; diff --git a/Configuration/JavaScriptModules.php b/Configuration/JavaScriptModules.php new file mode 100644 index 0000000..c9e80a9 --- /dev/null +++ b/Configuration/JavaScriptModules.php @@ -0,0 +1,19 @@ + [ + 'backend', + 'core', + ], + 'imports' => [ + '@typo3/dashboard/' => [ + 'path' => 'EXT:dashboard/Resources/Public/JavaScript/', + 'exclude' => [ + 'EXT:dashboard/Resources/Public/JavaScript/Contrib/', + ], + ], + 'chart.js' => 'EXT:dashboard/Resources/Public/JavaScript/Contrib/chartjs.js', + // legacy, has been renamed "chart.js" + '@typo3/dashboard/contrib/chartjs.js' => 'EXT:dashboard/Resources/Public/JavaScript/Contrib/chartjs.js', + ], +]; diff --git a/Configuration/Services.php b/Configuration/Services.php new file mode 100644 index 0000000..69ee152 --- /dev/null +++ b/Configuration/Services.php @@ -0,0 +1,12 @@ +addCompilerPass(new DependencyInjection\DashboardWidgetPass('dashboard.widget')); +}; diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..600c2a7 --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,252 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + TYPO3\CMS\Dashboard\: + resource: '../Classes/*' + + TYPO3\CMS\Dashboard\Widgets\WidgetConfiguration: + autowire: false + + dashboard.buttons.syslogErrors: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.buttonText' + + dashboard.buttons.syslogUsers: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogUsers.buttonText' + $channel: 'user' + + cache.dashboard.rss: + class: 'TYPO3\CMS\Core\Cache\Frontend\FrontendInterface' + factory: ['@TYPO3\CMS\Core\Cache\CacheManager', 'getCache'] + arguments: + $identifier: 'dashboard_rss' + + dashboard.buttons.t3news: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems' + $link: 'https://news.typo3.com' + $target: '_blank' + + dashboard.buttons.t3securityAdvisories: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.moreItems' + $link: 'https://typo3.org/help/security-advisories' + $target: '_blank' + + dashboard.buttons.docGettingStarted: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.content.label' + $link: 'https://docs.typo3.org/m/typo3/tutorial-getting-started/main/en-us/Index.html' + $target: '_blank' + + dashboard.buttons.docTypoScriptReference: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.content.label' + $link: 'https://docs.typo3.org/m/typo3/reference-typoscript/main/en-us/Index.html' + $target: '_blank' + + dashboard.buttons.docTSconfig: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.content.label' + $link: 'https://docs.typo3.org/m/typo3/reference-tsconfig/main/en-us/Index.html' + $target: '_blank' + + dashboard.widget.rss: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + tags: + - name: dashboard.widget + identifier: 'rss' + groupNames: 'news' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.description' + iconIdentifier: 'content-widget-rss' + height: 'large' + width: 'medium' + + dashboard.widget.t3news: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + arguments: + $buttonProvider: '@dashboard.buttons.t3news' + $options: + feedUrl: 'https://typo3.org/rss' + tags: + - name: dashboard.widget + identifier: 't3news' + groupNames: 'news' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description' + iconIdentifier: 'content-widget-rss' + height: 'large' + width: 'medium' + + dashboard.widget.sysLogErrors: + class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget' + arguments: + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider' + $buttonProvider: '@dashboard.buttons.syslogErrors' + $options: + refreshAvailable: true + tags: + - name: dashboard.widget + identifier: 'sysLogErrors' + groupNames: 'systemInfo' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.description' + iconIdentifier: 'content-widget-chart-bar' + height: 'medium' + width: 'medium' + + dashboard.widget.docGettingStarted: + class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget' + arguments: + $buttonProvider: '@dashboard.buttons.docGettingStarted' + $options: + text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text' + tags: + - name: dashboard.widget + identifier: 'docGettingStarted' + groupNames: 'documentation' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.description' + iconIdentifier: 'content-widget-text' + height: 'small' + + dashboard.widget.docTypoScriptReference: + class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget' + arguments: + $buttonProvider: '@dashboard.buttons.docTypoScriptReference' + $options: + text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.text' + tags: + - name: dashboard.widget + identifier: 'docTypoScriptReference' + groupNames: 'documentation' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.typoscriptReference.description' + iconIdentifier: 'content-widget-text' + height: 'small' + + dashboard.widget.docTSconfig: + class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget' + arguments: + $buttonProvider: '@dashboard.buttons.docTSconfig' + $options: + text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.text' + tags: + - name: dashboard.widget + identifier: 'docTSconfig' + groupNames: 'documentation' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.TSconfigReference.description' + iconIdentifier: 'content-widget-text' + height: 'small' + + dashboard.widget.t3information: + class: 'TYPO3\CMS\Dashboard\Widgets\T3GeneralInformationWidget' + tags: + - name: dashboard.widget + identifier: 't3information' + groupNames: 'general' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3information.description' + iconIdentifier: 'content-widget-text' + height: 'medium' + width: 'medium' + + dashboard.widget.typeOfUsers: + class: 'TYPO3\CMS\Dashboard\Widgets\DoughnutChartWidget' + arguments: + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\TypeOfUsersChartDataProvider' + tags: + - name: dashboard.widget + identifier: 'typeOfUsers' + groupNames: 'systemInfo' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.description' + iconIdentifier: 'content-widget-chart-pie' + height: 'medium' + + dashboard.widget.t3securityAdvisories: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + arguments: + $buttonProvider: '@dashboard.buttons.t3securityAdvisories' + $options: + feedUrl: 'https://typo3.org/rss-security' + tags: + - name: dashboard.widget + identifier: 't3securityAdvisories' + groupNames: 'news' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3securityAdvisories.description' + iconIdentifier: 'content-widget-rss' + height: 'large' + width: 'medium' + + dashboard.widget.failedLogins: + class: 'TYPO3\CMS\Dashboard\Widgets\NumberWithIconWidget' + arguments: + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\NumberOfFailedLoginsDataProvider' + $options: + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title' + subtitle: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle' + icon: 'content-elements-login' + tags: + - name: dashboard.widget + identifier: 'failedLogins' + groupNames: 'systemInfo' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.description' + iconIdentifier: 'content-widget-number' + + dashboard.widget.bookmarks: + class: 'TYPO3\CMS\Dashboard\Widgets\BookmarksWidget' + tags: + - name: dashboard.widget + identifier: 'bookmarks' + groupNames: 'content' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_bookmarks.xlf:widget.bookmarks.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_bookmarks.xlf:widget.bookmarks.description' + iconIdentifier: 'content-bookmark' + height: 'medium' + width: 'medium' + + dashboard.widget.latestChangedPages: + class: 'TYPO3\CMS\Dashboard\Widgets\LatestChangedPagesWidget' + arguments: + $options: + refreshAvailable: true + tags: + - name: dashboard.widget + identifier: 'latestChangedPages' + groupNames: 'content' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.description' + iconIdentifier: 'content-widget-list' + height: 'medium' + width: 'medium' + + dashboard.widget.latestBeLogins: + class: 'TYPO3\CMS\Dashboard\Widgets\LatestBeLoginsWidget' + arguments: + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\LatestBeLoginsDataProvider' + $buttonProvider: '@dashboard.buttons.syslogUsers' + tags: + - name: dashboard.widget + identifier: 'latestBeLogins' + groupNames: 'systemInfo' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestBeLogins.description' + iconIdentifier: 'content-widget-list' + height: 'medium' + width: 'small' diff --git a/Configuration/TCA/Overrides/be_groups.php b/Configuration/TCA/Overrides/be_groups.php new file mode 100644 index 0000000..5279cf2 --- /dev/null +++ b/Configuration/TCA/Overrides/be_groups.php @@ -0,0 +1,26 @@ + [ + 'label' => 'core.tca:availableWidgets', + 'config' => [ + 'type' => 'select', + 'renderType' => 'selectCheckBox', + 'itemsProcFunc' => \TYPO3\CMS\Dashboard\WidgetRegistry::class . '->widgetItemsProcFunc', + 'size' => 5, + 'autoSizeMax' => 50, + ], + ], + + ]; + \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('be_groups', $additionalColumns); + \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( + 'be_groups', + 'availableWidgets', + '', + 'after:groupMods' + ); +}); diff --git a/Configuration/TCA/be_dashboards.php b/Configuration/TCA/be_dashboards.php new file mode 100644 index 0000000..a7b356c --- /dev/null +++ b/Configuration/TCA/be_dashboards.php @@ -0,0 +1,63 @@ + [ + 'title' => 'dashboard.db:be_dashboard', + 'label' => 'title', + 'tstamp' => 'tstamp', + 'crdate' => 'crdate', + 'adminOnly' => true, + 'rootLevel' => 1, + 'delete' => 'deleted', + 'hideTable' => true, + 'enablecolumns' => [ + 'disabled' => 'hidden', + 'starttime' => 'starttime', + 'endtime' => 'endtime', + ], + 'default_sortby' => 'crdate DESC', + 'typeicon_classes' => [ + 'default' => 'content-dashboard', + ], + ], + 'columns' => [ + // The owner of the dashboard + 'cruser_id' => [ + 'config' => [ + 'type' => 'passthrough', + ], + ], + 'identifier' => [ + 'label' => 'dashboard.db:identifier', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'max' => 255, + 'required' => true, + ], + ], + 'title' => [ + 'label' => 'dashboard.db:title', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'max' => 255, + 'required' => true, + ], + ], + ], + 'types' => [ + '1' => [ + 'showitem' => ' + --div--;core.form.tabs:general, + identifier,title, + --div--;core.form.tabs:access, + hidden, --palette--;;timeRestriction, + --div--;core.form.tabs:extended, + ', + ], + ], + 'palettes' => [ + 'timeRestriction' => ['showitem' => 'starttime, endtime'], + ], +]; diff --git a/Documentation/Configuration/Index.rst b/Documentation/Configuration/Index.rst new file mode 100644 index 0000000..4b8b84b --- /dev/null +++ b/Documentation/Configuration/Index.rst @@ -0,0 +1,20 @@ +.. include:: /Includes.rst.txt + +.. _configuration: + +============= +Configuration +============= + +Target group: **Developers** and **Integrators** + +.. toctree:: + :maxdepth: 3 + :titlesonly: + + WidgetRegistration + WidgetGroupCreation + WidgetPresets + WidgetSettings + WidgetTemplate + PermissionHandlingOfWidgets diff --git a/Documentation/Configuration/PermissionHandlingOfWidgets.rst b/Documentation/Configuration/PermissionHandlingOfWidgets.rst new file mode 100644 index 0000000..c319bfb --- /dev/null +++ b/Documentation/Configuration/PermissionHandlingOfWidgets.rst @@ -0,0 +1,17 @@ +.. include:: /Includes.rst.txt + +.. _permission-handling-of-widgets: + +====================== +Permissions of widgets +====================== + +Backend users marked as administrator have always access to all registered widgets. + +Other backend users can be restricted via :guilabel:`Access List > Dashboard widgets` inside of user groups. +Each widget needs to be explicitly allowed. + +.. figure:: /Images/AccessRestriction.png + :align: center + + Granting access to dashboard widgets for backend users. diff --git a/Documentation/Configuration/WidgetGroupCreation.rst b/Documentation/Configuration/WidgetGroupCreation.rst new file mode 100644 index 0000000..b521ac1 --- /dev/null +++ b/Documentation/Configuration/WidgetGroupCreation.rst @@ -0,0 +1,45 @@ +.. include:: /Includes.rst.txt + +.. _create-widget-group: + +=================== +Create widget group +=================== + +Widget groups are used to group widgets into tabs. +This will have an effect when adding new widgets to an dashboard. +See :ref:`adding-widgets` to get an idea of the UI. + +Groups are defined as PHP array: + +.. code-block:: php + :caption: Example from EXT:dashboard/Configuration/Backend/DashboardWidgetGroups.php + + [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.general', + ], + 'systemInfo' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.system', + ], + 'typo3' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.typo3', + ], + 'news' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.news', + ], + 'documentation' => [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widget_group.documentation', + ], + ]; + +The file has to return an array of groups. +Each group consists of an array key used as identifier and an single option :php:`title`. +The title will be processed through translation and can be an ``LLL`` reference. + +Each extension can create arbitrary widget groups. + +Widgets can be assigned to multiple groups using the :confval:`widget-tag-groupNames`. +Please read :ref:`register-new-widget` to understand how this is done. diff --git a/Documentation/Configuration/WidgetPresets.rst b/Documentation/Configuration/WidgetPresets.rst new file mode 100644 index 0000000..a9d31ee --- /dev/null +++ b/Documentation/Configuration/WidgetPresets.rst @@ -0,0 +1,124 @@ +.. include:: /Includes.rst.txt + +.. _dashboard-presets: + +================= +Dashboard Presets +================= + +It is possible to configure presets of dashboards. +The extension already ships a ``default`` as well as an ``empty`` dashboard preset. + +.. _create-preset: + +Create preset +------------- + +New presets can be configured: + +.. code-block:: php + :caption: Example from EXT:dashboard/Configuration/Backend/DashboardPresets.php + + [ + 'title' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default', + 'description' => 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:dashboard.default.description', + 'iconIdentifier' => 'content-dashboard', + 'defaultWidgets' => [ + 't3information', + 't3news', + 'docGettingStarted', + [ + 'identifier' => 'rss', + 'settings' => [ + 'label' => 'My RSS Feed', + 'feedUrl' => 'https://typo3.org/rss', + 'limit' => 10, + ], + ], + ], + 'showInWizard' => false, + ], + ]; + +The file has to return an array with all presets. +Each prefix itself is an array, with an identifier as key. +The identifier is used to configure presets for users, see :ref:`configure-preset-for-user`. + +Each preset consists of the following options: + +.. php:class:: TYPO3\CMS\Dashboard\DashboardPreset + +.. confval:: title + :type: string + :name: widget-presets-title + + The title used for the preset. E.g. a ``LLL:EXT:`` reference.. + +.. confval:: description + :type: string + :name: widget-presets-description + + The description used for the preset. E.g. a ``LLL:EXT:`` reference.. + +.. confval:: iconIdentifier + :type: string + :name: widget-presets-iconIdentifier + + The identifier of the icon to use. + +.. confval:: defaultWidgets + :type: array + :name: widget-presets-defaultWidgets + + An array of widget identifiers, or fine-grained structure, that should be part of the dashboard preset. + + Widgets are always filtered by permissions of each user. + Only widgets with access are actually part of the dashboard. + Have a look at :ref:`permission-handling-of-widgets` to understand how to handle permissions. + +.. confval:: showInWizard + :type: bool + :name: widget-presets-showInWizard + + Boolean value to indicate, whether this preset should be visible in the wizard, + when creating new dashboards, see :ref:`adding-dashboard`. + + This can be disabled, to add presets via :ref:`configure-preset-for-user`, without + showing up in the wizard. + +.. _configure-preset-for-user: + +Configure preset for user +------------------------- + +To define the default preset for a backend user, the following User TSconfig can be added: + +.. code-block:: typoscript + + options.dashboard.dashboardPresetsForNewUsers = default + +Where ``default`` is the identifier of the preset. +Even a comma separated list of identifiers is possible: + +.. code-block:: typoscript + + options.dashboard.dashboardPresetsForNewUsers = default, companyDefault + +It is also possible to add another dashboard to the set of dashboards: + +.. code-block:: typoscript + + options.dashboard.dashboardPresetsForNewUsers := addToList(anotherOne) + +If nothing is configured, ``default`` will be used as identifier. + +.. seealso:: + + :ref:`t3tsref:userthetsconfigfield` section of TSconfig manual + explains how to set or register TSconfig for user. + + :ref:`t3tsref:typoscript-syntax-syntax-value-modification` explains the usage of + :typoscript:`:=` TypoScript operator. diff --git a/Documentation/Configuration/WidgetRegistration.rst b/Documentation/Configuration/WidgetRegistration.rst new file mode 100644 index 0000000..feb77bb --- /dev/null +++ b/Documentation/Configuration/WidgetRegistration.rst @@ -0,0 +1,326 @@ +.. include:: /Includes.rst.txt + +Widgets need to be provided by an extension, e.g. by ext:dashboard. +They are provided as a PHP class with specific feature sets. +Each of the widgets can be registered with different configurations as documented below. + +.. include:: /Shared/DifferenceRegistrationAndImplementation.rst.txt + +The below example will use the RSS Widget as a concrete example. + +.. _register-new-widget: + +=================== +Register new Widget +=================== + +Registration happens through :ref:`Dependency Injection ` +either in :file:`Services.yaml` or :file:`Services.php`. +Both files can exist and will be merged. + +:file:`Services.yaml` is recommended and easier to write, +while :file:`Services.php` provide way more flexibility. + +.. _register-new-widget-naming: + +Naming widgets +-------------- + +Widgets receive a name in form of ``dashboard.widget.vendor.ext_key.widgetName``. + +``vendor`` + Should be a snaked version of composer vendor. + +``ext_key`` + Should be the extension key. + +This prevents naming conflicts if multiple 3rd Party extensions are installed. + +.. _register-new-widget-services: + +Services.yaml file +------------------ + +In order to turn the PHP class :php:`\TYPO3\CMS\Dashboard\Widgets\RssWidget` into an actual widget, +the following service registration can be used: + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Services.yaml + + services: + _defaults: + autowire: true + autoconfigure: true + public: false + + TYPO3\CMS\Dashboard\: + resource: '../Classes/*' + + dashboard.widget.t3news: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + arguments: + $buttonProvider: '@dashboard.buttons.t3news' + $options: + feedUrl: 'https://www.typo3.org/rss' + tags: + - name: dashboard.widget + identifier: 't3news' + groupNames: 'news' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description' + iconIdentifier: 'content-widget-rss' + height: 'large' + width: 'medium' + +The beginning of the file is not related to the widget itself, but dependency injection in general, +see: :ref:`t3coreapi:configure-dependency-injection-in-extensions`. + +.. _register-new-widget-service-configuration: + +Service configuration +""""""""""""""""""""" + +The last block configured a service called :yaml:`dashboard.widget.t3news`. + +This service is configured to use the existing PHP class :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget`. +When creating the instance of this class, an array is provided for the constructor argument :php:`$options`. +This way the same PHP class can be used with different configuration to create new widgets. + +The following keys are defined for the service: + +.. confval:: class + :type: string + :name: widget-class + :Example: :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget` + + Defines the concrete PHP class to use as the implementation of the widget. + +.. confval:: arguments + :type: map + :name: widget-arguments + + A set of key-value pairs, where the keys are the argument names and the + values are the corresponding argument values. The specific arguments depend + on the widget being configured, and each widget can define custom arguments. + + Documentation for the provided widgets is available at :ref:`widgets`. + +.. confval:: tags + :type: array of dictionaries + :name: widget-tags + + Registers the service as an actual widget for :composer:`typo3/cms-dashboard`. Each entry in + the array is a dictionary that can include various properties like name, + identifier, groupNames, and so on, used to categorize and identify the widget. + + See :ref:`register-new-widget-tags-section`. + +.. _register-new-widget-tags-section: + +Tags Section +"""""""""""" + +In order to turn the instance into a widget, the tag `dashboard.widget` is configured in `tags` section. +The following options are mandatory and need to be provided: + +.. confval:: name + :type: string + :name: widget-tag-name + :required: + :Example: `dashboard.widget` + + Always has to be `dashboard.widget`. + Defines that this tag configures the service to be registered as a widget for + ext:dashboard. + +.. confval:: identifier + :type: string + :name: widget-tag-identifier + :required: + :Example: `t3news` + + Used to store which widgets are currently assigned to dashboards. + Furthermore, it is used to allow access control, see :ref:`permission-handling-of-widgets`. + +.. confval:: groupNames + :type: string (comma-separated) + :name: widget-tag-groupNames + :required: + :Example: `news` + + Defines which groups should contain the widget. + Used when adding widgets to a dashboard to group related widgets in tabs. + Multiple names can be defined as a comma-separated string, e.g.: `typo3, general`. + + See :ref:`create-widget-group` regarding how to create new widget groups. + There is no difference between custom groups and existing groups. + Widgets are registered to all groups by their name. + +.. confval:: title + :type: string (language reference) + :name: widget-tag-title + :required: + :Example: `LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title` + + Defines the title of the widget. Language references are resolved. + +.. confval:: description + :type: string (language reference) + :name: widget-tag-description + :required: + :Example: `LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description` + + Defines the description of the widget. Language references are resolved. + +.. confval:: iconIdentifier + :type: string + :name: widget-tag-iconIdentifier + :required: + :Example: `content-widget-rss` + + One of the registered icons. + Icons can be registered through :ref:`t3coreapi:icon`. + +The following options are optional and have default values which will be used if not defined: + +.. confval:: height + :type: string + :name: widget-tag-height + :Example: `large` + + Has to be a string value: `large`, `medium`, or `small`. + +.. confval:: width + :type: string + :name: widget-tag-width + :Example: `medium` + + Has to be a string value: `large`, `medium`, or `small`. + +.. _register-new-widget-splitting: + +Splitting up Services.yaml +-------------------------- + +In case the :file:`Services.yaml` is getting to large, it can be split up. +The official documentation can be found at `symfony.com `__. +An example to split up all Widget related configuration would look like: + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Services.yaml + + imports: + - { resource: Backend/DashboardWidgets.yaml } + +.. note:: + + Note that you have to repeat all necessary information, e.g. :yaml:`services:` section with :yaml:`_defaults:` again. + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Backend/DashboardWidgets.yaml + + services: + _defaults: + autowire: true + autoconfigure: true + public: false + + TYPO3\CMS\Dashboard\Widgets\: + resource: '../Classes/Widgets/*' + + dashboard.buttons.t3news: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems' + $link: 'https://news.typo3.com' + $target: '_blank' + + dashboard.widget.t3news: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + arguments: + $buttonProvider: '@dashboard.buttons.t3news' + $options: + feedUrl: 'https://www.typo3.org/rss' + tags: + - name: dashboard.widget + identifier: 't3news' + groupNames: 'news' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.description' + iconIdentifier: 'content-widget-rss' + height: 'large' + width: 'medium' + + +.. _register-new-widget-services-php: + +Services.php File +----------------- + +This is not intended for integrators but developers only, as this involves PHP experience. + +The typical use case should be solved via :file:`Services.yaml`. +But for more complex situations, it is possible to register widgets via :file:`Services.php`. +Even if :file:`Services.php` contains PHP, it is only executed during compilation of the dependency injection container. +Therefore, it is not possible to check for runtime information like URLs, users, configuration or packages. + +Instead, this approach can be used to register widgets only if their service dependencies are available. +The :php:`ContainerBuilder` instance provides a method :php:`hasDefinition()` +that may be used to check for optional dependencies. +Make sure to declare the optional dependencies in :file:`composer.json` as +suggested extensions to ensure packages are ordered correctly in order for +services to be registered with deterministic ordering. + +The following example demonstrates how a widget can be registered via :file:`Services.php`: + +.. code-block:: php + + services(); + + if ($containerBuilder->hasDefinition(Status::class)) { + $services->set('widgets.dashboard.widget.exampleWidget') + ->class(ExampleWidget::class) + ->arg('$buttonProvider', new Reference(ExampleProvider::class)) + ->arg('$options', ['template' => 'Widget/ExampleWidget']) + ->tag('dashboard.widget', [ + 'identifier' => 'widgets-exampleWidget', + 'groupNames' => 'systemInfo', + 'title' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:widgets.dashboard.widget.exampleWidget.title', + 'description' => 'LLL:EXT:ext_key/Resources/Private/Language/locallang.xlf:widgets.dashboard.widget.exampleWidget.description', + 'iconIdentifier' => 'content-widget-list', + 'height' => 'medium', + 'width' => 'medium' + ]) + ; + } + }; + +Above example will register a new widget called ``widgets.dashboard.widget.exampleWidget``. +The widget is only registered, in case the extension "reports" is enabled, which +results in the availablity of the :php:`TYPO3\CMS\Report\Status` during container compile time. + +Configuration is done in the same way as with :file:`Services.yaml`, except a PHP API is used. +The :php:`new Reference` equals to :yaml:`@` inside the YAML, to reference another service. +:yaml:`arguments:` are registered via :php:`->arg()` method call. +And :yaml:`tags:` are added via :php:`->tag()` method call. + +Using this approach, it is possible to provide widgets that depend on 3rd party code, +without requiring this 3rd party code. +Instead the 3rd party code can be suggested and is supported if its installed. + +Further information regarding how :file:`Services.php` works in general, can be found +at `symfony.com `_. +Make sure to switch code examples from YAML to PHP. diff --git a/Documentation/Configuration/WidgetSettings.rst b/Documentation/Configuration/WidgetSettings.rst new file mode 100644 index 0000000..7486880 --- /dev/null +++ b/Documentation/Configuration/WidgetSettings.rst @@ -0,0 +1,59 @@ +.. include:: /Includes.rst.txt + +.. _settings: + +===================================== +Adjust settings of registered widgets +===================================== + +.. versionadded:: 14.0 + `Configurable Dashboard Widgets `_ + have been introduced with TYPO3 14.0. + +.. contents:: Table of contents + +.. _adjust-settings-of-widget-why: +.. _configurable-widgets: + +Configurable dashboard widgets +------------------------------ + +.. versionadded:: 14.0 + +Dashboard widgets can be configured on a per-instance level using the Settings +API. This allows widget authors to define configurable settings that editors +can modify directly from the dashboard interface, making widgets more +flexible and user-friendly. + +Examples are URLs for RSS feeds, limits on displayed items, or categories for +filtering content. + +Each widget instance maintains its own configuration, enabling multiple +instances of the same widget type with different settings on the same or +different dashboards. + +Configurable widgets display a `settings (cog) icon `_ +and allow editors to configure the widget in a modal dialog. + +Extension authors can implement :php-short:`\TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface` +to make their widgets configurable: +`Configurable dashboard widget implementation `_. + +.. _adjust-settings-of-widget: + +Adjust settings of registered widgets +===================================== + +Each widget is registered with an identifier, and all :file:`Services.*` files are merged. +Therefore it is possible to override widgets. +In order to override, the extension which should override has to be loaded after the extension that registered the widget. + +Concrete options depend on the widget to configure. +Each widget should provide documentation covering all possible options and their meaning. +For delivered widgets by EXT:dashboard see :ref:`widgets`. + +In case a widget defined by EXT:dashboard should be adjusted, +the extension has to define a dependency to EXT:dashboard. + +Afterwards the widget can be registered again, with different options. See +:ref:`register-new-widget` to get an in depth example of how to register a widget. diff --git a/Documentation/Configuration/WidgetTemplate.rst b/Documentation/Configuration/WidgetTemplate.rst new file mode 100644 index 0000000..cfb6478 --- /dev/null +++ b/Documentation/Configuration/WidgetTemplate.rst @@ -0,0 +1,26 @@ +.. include:: /Includes.rst.txt + +.. _adjust-template-of-widget: + +========================== +Adjust template of widgets +========================== + +When adding own widgets, it might be necessary to provide custom templates. +In such a case the file path containing the template files needs to be added. + +This is done using a :file:`Configuration/page.tsconfig` file, see +:doc:`changelog ` and +:doc:`changelog ` +for details on this: + +.. code-block:: typoscript + + # Pattern: templates.typo3/cms-dashboard."something-unique" = "overriding-extension-composer-name":"entry-path" + templates.typo3/cms-dashboard.1644485473 = myvendor/myext:Resources/Private + +A template file can then be added to path :file:`Resources/Private/Templates/Widgets/MyExtensionsGreatWidget.html` +and is referenced in the PHP class using :php:`->render('Widgets/MyExtensionsGreatWidget');`. The registration +into namespace :php:`typo3/cms-dashboard` is shared between all extensions. It is thus a good idea to give +template file names unique names (for instance by prefixing them with the extension name), to avoid situations +where templates from multiple extensions that provide different widgets override each other. diff --git a/Documentation/Developer/AddingButtons.rst b/Documentation/Developer/AddingButtons.rst new file mode 100644 index 0000000..b9ac3ea --- /dev/null +++ b/Documentation/Developer/AddingButtons.rst @@ -0,0 +1,111 @@ +.. include:: /Includes.rst.txt + +.. _adding-buttons: + +======================= +Adding button to Widget +======================= + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets + +In order to add a button to a widget, a new dependency to an :php:`ButtonProviderInterface` can be added. + +.. _adding-buttons-template: + +Template +-------- + +The output itself is done inside of the Fluid template, for example :file:`Resources/Private/Templates/Widget/RssWidget.html`: + +.. code-block:: html + + + + {f:translate(id: button.title, default: button.title)} + + + +.. _adding-buttons-configuration: + +Configuration +------------- + +The configuration is done through an configured Instance of the dependency, for example :file:`Services.yaml`: + +.. code-block:: yaml + + services: + # … + + dashboard.buttons.t3news: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems' + $link: 'https://news.typo3.com' + $target: '_blank' + + dashboard.widget.t3news: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + arguments: + # … + $buttonProvider: '@dashboard.buttons.t3news' + # … + +See also: :php:`\TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider`. + +.. confval:: $title + :type: string + :name: button-title + + The title used for the button. E.g. an ``LLL:EXT:`` reference like + ``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``. + +.. confval:: $link + :type: string + :name: button-link + + The link to use for the button. Clicking the button will open the link. + +.. confval:: $target + :type: string + :name: button-target + + The target of the link, e.g. ``_blank``. + ``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``. + + +.. _adding-buttons-implementation: + +Implementation +-------------- + +An example implementation could look like this: + +.. code-block:: php + :caption: Classes/Widgets/RssWidget.php + + class RssWidget implements WidgetInterface + { + public function __construct( + // … + private readonly ButtonProviderInterface $buttonProvider = null, + // … + ) { + } + + public function renderWidgetContent(): string + { + // … + $this->view->assignMultiple([ + // … + 'button' => $this->buttonProvider, + // … + ]); + // … + } + + public function getOptions(): array + { + return $this->options; + } + } diff --git a/Documentation/Developer/ConfigurableWidgets.rst b/Documentation/Developer/ConfigurableWidgets.rst new file mode 100644 index 0000000..9b97325 --- /dev/null +++ b/Documentation/Developer/ConfigurableWidgets.rst @@ -0,0 +1,34 @@ +:navigation-title: Configurable widgets + +.. include:: /Includes.rst.txt + +.. _configurable-widget-implementation: + +============================================ +Configurable dashboard widget implementation +============================================ + +.. versionadded:: 14.0 + `Configurable Dashboard Widgets `_ + have been introduced with TYPO3 14.0. + +Widget authors can implement configurable widgets by using to the +renderer interface :php:`TYPO3\CMS\Dashboard\Widgets\WidgetRendererInterface` +which allows to defining settings in their widget renderer. + +Settings are automatically validated and processed using the Settings API. +All types that are available for site settings definition are available: +`Definition types `_. + +.. seealso:: + :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget` is a configurable widget + implementation. + +.. _configurable-widget-implementation-example: + +Example: A configurable widget implementation +============================================= + +.. literalinclude:: _codesnippets/_ConfigurableWidget.php.inc + :language: php + :caption: EXT:my_extension/Classes/Widgets/ConfigurableWidget.php diff --git a/Documentation/Developer/GraphWidgetImplementation.rst b/Documentation/Developer/GraphWidgetImplementation.rst new file mode 100644 index 0000000..173710a --- /dev/null +++ b/Documentation/Developer/GraphWidgetImplementation.rst @@ -0,0 +1,174 @@ +.. include:: /Includes.rst.txt + +.. _graph-widget-implementation: + +====================== +Implement graph widget +====================== + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets + +First of all a new data provider is required, which will provide the data for the chart. +Next the data will be provided to the widget instance, which will be rendered with JavaScript modules and Css. + +To make the dashboard aware of this workflow, some interfaces come together: + +* :php:`EventDataInterface` + +* :php:`AdditionalCssInterface` + +Also the existing template file :file:`Widget/ChartWidget` is used, which provides necessary HTML to render the chart. +The provided ``eventData`` will be rendered as a chart and therefore has to match the expected structure. + +An example would be :file:`Classes/Widgets/BarChartWidget.php`: + +.. code-block:: php + :caption: Classes/Widgets/BarChartWidget.php + + class BarChartWidget implements WidgetInterface, EventDataInterface, AdditionalCssInterface + { + public function __construct( + // … + private readonly ChartDataProviderInterface $dataProvider, + // … + ) { + // … + $this->dataProvider = $dataProvider; + // … + } + + public function renderWidgetContent(): string + { + // … + $this->view->assignMultiple([ + // … + 'configuration' => $this->configuration, + // … + ]); + // … + } + + public function getEventData(): array + { + return [ + 'graphConfig' => [ + 'type' => 'bar', + 'options' => [ + // … + ], + 'data' => $this->dataProvider->getChartData(), + ], + ]; + } + + public function getCssFiles(): array + { + return []; + } + + public function getOptions(): array + { + return $this->options; + } + } + +Together with :file:`Services.yaml`: + +.. code-block:: yaml + + services: + dashboard.widget.sysLogErrors: + class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget' + arguments: + # … + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider' + # … + tags: + - name: dashboard.widget + +The configuration adds necessary CSS classes, as well as the ``dataProvider`` to use. +The provider implements :php:`ChartDataProviderInterface` and could look like the following. + +.. code-block:: php + :caption: Classes/Widgets/Provider/SysLogErrorsDataProvider + + class SysLogErrorsDataProvider implements ChartDataProviderInterface + { + /** + * Number of days to gather information for. + * + * @var int + */ + protected $days = 31; + + /** + * @var array + */ + protected $labels = []; + + /** + * @var array + */ + protected $data = []; + + public function __construct(int $days = 31) + { + $this->days = $days; + } + + public function getChartData(): array + { + $this->calculateDataForLastDays(); + return [ + 'labels' => $this->labels, + 'datasets' => [ + [ + 'label' => $this->getLanguageService()->sL('LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.chart.dataSet.0'), + 'backgroundColor' => WidgetApi::getDefaultChartColors()[0], + 'border' => 0, + 'data' => $this->data + ] + ] + ]; + } + + protected function getNumberOfErrorsInPeriod(int $start, int $end): int + { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log'); + return (int)$queryBuilder + ->count('*') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'type', + $queryBuilder->createNamedParameter(SystemLogType::ERROR, Connection::PARAM_INT) + ), + $queryBuilder->expr()->gte( + 'tstamp', + $queryBuilder->createNamedParameter($start, Connection::PARAM_INT) + ), + $queryBuilder->expr()->lte( + 'tstamp', + $queryBuilder->createNamedParameter($end, Connection::PARAM_INT) + ) + ) + ->execute() + ->fetchColumn(); + } + + protected function calculateDataForLastDays(): void + { + $format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?: 'Y-m-d'; + for ($daysBefore = $this->days; $daysBefore >= 0; $daysBefore--) { + $this->labels[] = date($format, strtotime('-' . $daysBefore . ' day')); + $startPeriod = strtotime('-' . $daysBefore . ' day 0:00:00'); + $endPeriod = strtotime('-' . $daysBefore . ' day 23:59:59'); + $this->data[] = $this->getNumberOfErrorsInPeriod($startPeriod, $endPeriod); + } + } + + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } + } diff --git a/Documentation/Developer/Index.rst b/Documentation/Developer/Index.rst new file mode 100644 index 0000000..f3e8986 --- /dev/null +++ b/Documentation/Developer/Index.rst @@ -0,0 +1,25 @@ +.. include:: /Includes.rst.txt + +.. _for-developer: + +============== +For Developers +============== + +Target group: **Developers** + +Welcome to our small dashboard introduction. +We will explain how to create widget groups and implement widgets. + +.. include:: /Shared/DifferenceRegistrationAndImplementation.rst.txt + +.. toctree:: + :maxdepth: 3 + :titlesonly: + + WidgetImplementation + ConfigurableWidgets + MakeRefreshable + AddingButtons + GraphWidgetImplementation + Interfaces diff --git a/Documentation/Developer/Interfaces.rst b/Documentation/Developer/Interfaces.rst new file mode 100644 index 0000000..1ff0358 --- /dev/null +++ b/Documentation/Developer/Interfaces.rst @@ -0,0 +1,178 @@ +.. include:: /Includes.rst.txt + +.. _interfaces: + +========== +Interfaces +========== + +The following list provides information for all necessary interfaces that are used inside of this documentation. +For up to date information, please check the source code. + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets + +.. php:class:: WidgetInterface + + Has to be implemented by all widgets. + This interface defines public API used by ext:dashboard to interact with widgets. + + .. php:method:: renderWidgetContent() + + :returntype: string + :returns: The rendered HTML to display. + + .. php:method:: getOptions() + + :returntype: array + :returns: The options of the widget as set in the registration. + +.. php:class:: RequestAwareWidgetInterface + + This interface declares a widget has a dependency to the current PSR-7 request. + When implemented, the dashboard controller will call :php:`setRequest()` immediately + after widget instantiation to hand over the current request. Widgets that rely on + :php:`BackendViewFactory` typically need the current request. + + .. php:method:: setRequest(ServerRequestInterface $request) + + :returntype: void + +.. php:class:: WidgetConfigurationInterface + + Used internally in ext:dashboard. + Used to separate internal configuration from widgets. + Can be required in widget classes and passed to view. + + .. php:method:: getIdentifier() + + :returntype: string + :returns: Unique identifer of a widget. + + .. php:method:: getServiceName() + + :returntype: string + :returns: Service name providing the widget implementation. + + .. php:method:: getGroupNames() + + :returntype: array + :returns: Group names associated to this widget. + + .. php:method:: getTitle() + + :returntype: string + :returns: Title of a widget, this is used for the widget selector. + + .. php:method:: getDescription() + + :returntype: string + :returns: Description of a widget, this is used for the widget selector. + + .. php:method:: getIconIdentifier() + + :returntype: string + :returns: Icon identifier of a widget, this is used for the widget selector. + + .. php:method:: getHeight() + + :returntype: int + :returns: Height of a widget in rows (1-6). + + .. php:method:: getWidth() + + :returntype: int + :returns: Width of a widget in columns (1-4). + +.. php:class:: AdditionalJavaScriptInterface + + Widgets implementing this interface will add the provided JavaScript files. + Those files will be loaded in dashboard view if the widget is added at least once. + + .. php:method:: getJsFiles() + + Returns a list of JavaScript file names that should be included, e.g.: + + .. code-block:: php + + return [ + 'EXT:my_extension/Resources/Public/JavaScript/file.js', + 'EXT:my_extension/Resources/Public/JavaScript/file2.js', + ]; + + :returntype: array + :returns: List of JS files to load. + +.. php:class:: AdditionalCssInterface + + Widgets implementing this interface will add the provided Css files. + Those files will be loaded in dashboard view if the widget is added at least once. + + .. php:method:: getCssFiles() + + Returns a list of Css file names that should be included, e.g.: + + .. code-block:: php + + return [ + 'EXT:my_extension/Resources/Public/Css/widgets.css', + 'EXT:my_extension/Resources/Public/Css/list-widget.css', + ]; + + :returntype: array + :returns: List of Css files to load. + +.. php:class:: ButtonProviderInterface + + .. php:method:: getTitle() + + :returntype: string + :returns: The title used for the button. E.g. an ``LLL:EXT:`` reference like + ``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``. + + .. php:method:: getLink() + + :returntype: string + :returns: The link to use for the button. Clicking the button will open the link. + + .. php:method:: getTarget() + + :returntype: string + :returns: The target of the link, e.g. ``_blank``. + ``LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.t3news.moreItems``. + +.. php:class:: NumberWithIconDataProviderInterface + + .. php:method:: getNumber() + + :returntype: integer + :returns: The number to display for an number widget. + +.. php:class:: EventDataInterface + + .. php:method:: getEventData() + + :returntype: array + :returns: Returns data which should be send to the widget as JSON encoded value. + +.. php:class:: ChartDataProviderInterface + + .. php:method:: getChartData() + + :returntype: array + :returns: Provide the data for a graph. + The data and options you have depend on the type of chart. + More information can be found in the documentation of the specific type: + + Bar + https://www.chartjs.org/docs/latest/charts/bar.html#data-structure + + Doughnut + https://www.chartjs.org/docs/latest/charts/doughnut.html#data-structure + +.. php:class:: ListDataProviderInterface + + .. php:method:: getItems() + + :returntype: array + :returns: Provide the array if items. + Each entry should be a single string. diff --git a/Documentation/Developer/MakeRefreshable.rst b/Documentation/Developer/MakeRefreshable.rst new file mode 100644 index 0000000..7a6bc1b --- /dev/null +++ b/Documentation/Developer/MakeRefreshable.rst @@ -0,0 +1,83 @@ +.. include:: /Includes.rst.txt +.. _make-refreshable: + +================== +The refresh option +================== + +In each widget the refresh option can be enabled. If the option is enabled the +widget displays a reload button in the top right corner. It can then be +refreshed via user interaction or via a javascript api. + +To enable the refresh action button, you have to define the +:yaml:`refreshAvailable` option in the :yaml:`$options` part of the widget +registration. Below is an example of a RSS widget with the refresh option enabled. + +.. code-block:: yaml + + dashboard.widget.myOwnRSSWidget: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + arguments: + $options: + rssFile: 'https://typo3.org/rss' + lifeTime: 43200 + refreshAvailable: true + tags: + - name: dashboard.widget + identifier: 'myOwnRSSWidget' + groupNames: 'general' + title: 'LLL:EXT:extension/Resources/Private/Language/locallang.xlf:widgets.myOwnRSSWidget.title' + description: 'LLL:EXT:extension/Resources/Private/Language/locallang.xlf:widgets.myOwnRSSWidget.description' + iconIdentifier: 'content-widget-rss' + height: 'medium' + width: 'medium' + +.. note:: + + In this example, the TYPO3 core :php:`TYPO3\CMS\Dashboard\Widgets\RssWidget` + widget class is used. In case you have implemented own widget classes, you + have to implement the :php:`getOptions()` method, returning :php:`$this->options`, + to the corresponding classes. Otherwise the refresh option won't have any + effect. + +.. _refresh-button: + +Enable the refresh button +------------------------- + +Widgets can render a refresh button to allow users to manually refresh them. + +This is done by passing the value :php:`['refreshAvailable'] = true;` back +via :php:`getOptions()` method of the widget. + +All TYPO3 Core widgets implement this behaviour and allow integrators to +configure the option: + +.. include:: /Widgets/Options/RefreshAvailable.rst.txt + +.. _refresh-javascript: + +JavaScript API +-------------- + +It is possible for all widgets to dispatch an event, which will cause +the widget being refreshed. This is possible for all widgets on the dashboard +even when the :yaml:`refreshAvailable` option is not defined, or set to `false`. +This will give developers the option to refresh the widgets whenever they think +it is appropriate. + +To refresh a widget, dispatch the :js:`widgetRefresh` event on the +widget container (the :html:`div` element with the :html:`dashboard-item` class). +You can identify the container by the data attribute :html:`widget-hash`, which +is a unique hash for every widget, even if you have more widgets of the same +type on your dashboard. + +A small example below: + +.. code-block:: javascript + + document + .querySelector('[data-widget-hash="{your-unique-widget-hash}"]') + .dispatchEvent(new Event('widgetRefresh', {bubbles: true})); + +See :ref:`implement-new-widget-custom-js` to learn how to add custom JavaScript. diff --git a/Documentation/Developer/WidgetImplementation.rst b/Documentation/Developer/WidgetImplementation.rst new file mode 100644 index 0000000..d4ed7d4 --- /dev/null +++ b/Documentation/Developer/WidgetImplementation.rst @@ -0,0 +1,195 @@ +.. include:: /Includes.rst.txt + +.. _implement-new-widget: + +==================== +Implement new widget +==================== + +.. versionadded:: 14.0 + `Configurable Dashboard Widgets `_ + have been introduced with TYPO3 14.0. + + See also + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets + +.. seealso:: + + For information regarding registration of widgets, see: :ref:`register-new-widget`. + This section describes the implementation of new widgets for developers. + +Each extension can provide multiple Widgets. +ext:dashboard already ships with some widget implementations. + +Each widget has to be implemented as a PHP class. +The PHP class defines the concrete implementation and features of a widget, +while registration adds necessary options for a concrete instance of a widget. + +For example a TYPO3.org RSS Widget would consist of an :php:`RssWidget` PHP class. +This class would provide the implementation to fetch rss news and display them. +The concrete registration will provide the URL to RSS feed. + +.. _widget-php-class: + +PHP class +--------- + +Each Widget has to be a PHP class. +This class has to implement the :php:`WidgetInterface` and could look like this: + +.. code-block:: php + + class RssWidget implements WidgetInterface, RequestAwareWidgetInterface + { + private ServerRequestInterface $request; + + public function __construct( + private readonly WidgetConfigurationInterface $configuration, + private readonly Cache $cache, + private readonly BackendViewFactory $backendViewFactory, + private readonly ?ButtonProviderInterface $buttonProvider = null, + private readonly array $options = [] + ) { + } + + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } + + public function renderWidgetContent(): string + { + $view = $this->backendViewFactory->create($this->request); + $view->assignMultiple([ + 'items' => $this->getRssItems(), + 'options' => $this->options, + 'button' => $this->getButton(), + 'configuration' => $this->configuration, + ]); + return $view->render('Widget/RssWidget'); + } + + protected function getRssItems(): array + { + $items = []; + // Logic to populate $items array + return $items; + } + + public function getOptions(): array + { + return $this->options; + } + } + +The class should always provide documentation how to use in :file:`Services.yaml`. +The above class is documented at :ref:`rss-widget`. +The documentation should provide all possible options and an concrete example. +It should make it possible for integrators to register new widgets using the implementation. + +The difference between :php:`$options` and :php:`$configuration` in above example is the following: +:php:`$options` are the options for this implementation which can be provided through :file:`Services.yaml`. +:php:`$configuration` is an instance of :php:`WidgetConfigurationInterface` +holding all internal configuration, like icon identifier. + +.. _implement-new-widget-fluid: + +Using Fluid +----------- + +Most widgets will need a template. +Therefore each widget can define :php:`BackendViewFactory` as requirement for DI in +constructor, like done in RSS example. + + +.. _implement-new-widget-custom-js: + +Providing custom JS +------------------- + +There are two ways to add JavaScript for an widget: + +JavaScript module + Implement :php:`\TYPO3\CMS\Dashboard\Widgets\JavaScriptInterface`: + + .. code-block:: php + + + class ExampleChartWidget implements JavaScriptInterface + { + // ... + public function getJavaScriptModuleInstructions(): array + { + return [ + JavaScriptModuleInstruction::create( + '@myvendor/my-extension/module-name.js' + )->invoke('initialize'), + JavaScriptModuleInstruction::create( + '@myvendor/my-extension/module-name2.js' + )->invoke('initialize'), + ]; + } + } + + .. seealso:: + + :ref:`t3coreapi:backend-javascript-es6` for more info about JavaScript in TYPO3 Backend. + +Plain JS files + Implement :php:`AdditionalJavaScriptInterface`: + + .. code-block:: php + + class RssWidget implements WidgetInterface, AdditionalJavaScriptInterface + { + public function getJsFiles(): array + { + return [ + 'EXT:my_extension/Resources/Public/JavaScript/file.js', + 'EXT:my_extension/Resources/Public/JavaScript/file2.js', + ]; + } + } + +JavaScript + Implement :php:`\TYPO3\CMS\Dashboard\Widgets\JavaScriptInterface`: + + .. code-block:: php + + class ExampleChartWidget implements JavaScriptInterface + { + // ... + public function getJavaScriptModuleInstructions(): array + { + return [ + JavaScriptModuleInstruction::create( + '@typo3/dashboard/chart-initializer.js' + )->invoke('initialize'), + ]; + } + } + +All ways can be combined. + +.. _custom-css: + +Providing custom CSS +-------------------- + +It is possible to add custom Css to style widgets. + +Implement :php:`AdditionalCssInterface`: + +.. code-block:: php + + class RssWidget implements WidgetInterface, AdditionalCssInterface + { + public function getCssFiles(): array + { + return [ + 'EXT:my_extension/Resources/Public/Css/widgets.css', + 'EXT:my_extension/Resources/Public/Css/list-widget.css', + ]; + } + } diff --git a/Documentation/Developer/_codesnippets/_ConfigurableWidget.php.inc b/Documentation/Developer/_codesnippets/_ConfigurableWidget.php.inc new file mode 100644 index 0000000..096fe4c --- /dev/null +++ b/Documentation/Developer/_codesnippets/_ConfigurableWidget.php.inc @@ -0,0 +1,43 @@ +settings; + $title = $settings->get('title'); + $limit = $settings->get('limit'); + + // Use settings to customize widget output + return new WidgetResult( + content: '', + label: $title, + refreshable: true + ); + } +} diff --git a/Documentation/Editor/Index.rst b/Documentation/Editor/Index.rst new file mode 100644 index 0000000..1caef76 --- /dev/null +++ b/Documentation/Editor/Index.rst @@ -0,0 +1,167 @@ +.. include:: /Includes.rst.txt + +.. _for-editors: + +=========== +For Editors +=========== + +Target group: **Editors** + +Welcome to our small dashboard introduction. +We will explain the basic usage of the TYPO3 dashboard. + +.. _opening-dashboard: + +Opening Dashboard +================= + +By default the dashboard is opened when logging into the backend. + +The dashboard can be opened at any time by clicking the entry +:guilabel:`Dashboard` in the module menu. + +.. figure:: /Images/DashboardPosition.png + :align: center + + Open the dashboard by clicking on :guilabel:`Dashboard`. + +.. note:: + If the entry :guilabel:`Dashboard` is not visible in the menu there are two + possible causes: + + * You lack sufficient rights to view the dashboard. + * The system extension `dashboard` was not installed on your system. + + Ask your administrator about this. + +.. _adding-dashboard: + +Adding Dashboard +================ + +The EXT:dashboard allows to have multiple dashboards. +Switching between different dashboards is possible by using the corresponding tab. + +In order to add further dashboards, press the :guilabel:`+` sign. + +.. figure:: /Images/DashboardTabs.png + :align: center + + Tabs allowing to switch and add dashboards. + +A wizard should open which allows to add the new dashboard. + +There you can select a preset. At least the default preset, which is shipped +by core should be available. Depending on system configuration further dashboard +presets might be available. + +.. figure:: /Images/DashboardWizard.png + :align: center + + Wizard to add a new dashboard. + +.. _editing-dashboard: + +Editing Dashboard +================= + +Existing dashboards can be edited and deleted. +On the right side of the tab bar are the icons which allow deletion and adjusting +settings of the currently active dashboard. + +.. figure:: /Images/DashboardTabs.png + :align: center + + Icons on the right side of the tab bar allow adjusting settings or deletion of + the currently selected dashboard. + +.. _adding-widgets: + +Adding Widgets +============== + +Widgets can be added to a dashboard. +Dashboards which do not contain any widget yet, offer a dialog in the middle of +the screen, which allows to add one or more widgets to the current dashboard. + +All dashboards allow to add further widgets in the lower right corner through the +:guilabel:`+` Icon. + +.. figure:: /Images/AddWidget.png + :align: center + + Empty dashboard with possibilities to add new widgets. + +Once the action to add a new widget was triggered, a wizard opens which allows to +select the widget to add. + +Widgets are grouped in tabs and can be added by clicking on them. + +.. figure:: /Images/WidgetWizard.png + :align: center + + Wizard to select a new widget that will be added to the active dashboard. + +.. _widgets-configuration: + +Widget configuration +==================== + +.. versionadded:: 14.0 + `Configurable Dashboard Widgets `_ + have been introduced with TYPO3 14.0. + +* Dashboard widgets display a settings (cog) icon when they support configuration +* Clicking the settings icon opens a modal dialog with configurable options +* Settings are applied immediately after saving, with the widget content + refreshing automatically +* Each widget can be configured independently per user / per instance + + +.. figure:: /Images/DashboardConfiguration.png + :alt: Screenshot of the dashboard widget "RSS Feed" with the location of the settings (cog) icon + + Click the settings (cog) icon to configure a feed + +Extension authors can make their widgets configurable: +`Configurable dashboard widget implementation `_. + +.. _moving-widgets: + +Moving Widgets +============== + +Widgets can be moved around. Therefore a widget needs to be hovered. +If a widget is hovered some icons appear in the upper right corner of the widget. + +To move the widget, click and hold left mouse button on the cross icon. +Then move to the target position. + +.. figure:: /Images/WidgetMove.png + :align: center + + Widget in hover mode with additional icons in upper right corner. + +.. _deleting-widgets: + +Deleting Widgets +================ + +To delete a widget, the widget needs to be hovered. +If a widget is hovered some icons appear in the upper right corner of the widget. + +Click the trash icon which appears to delete the widget. + +.. figure:: /Images/WidgetMove.png + :align: center + + Widget in hover mode with additional icons in upper right corner. + +In order to prevent accidentally deletion, a modal is shown to confirm deletion. +Confirm by clicking the :guilabel:`Remove` button. + +.. figure:: /Images/WidgetDelete.png + :align: center + + Modal to confirm deletion of widget. diff --git a/Documentation/Images/AccessRestriction.png b/Documentation/Images/AccessRestriction.png new file mode 100644 index 0000000000000000000000000000000000000000..e63f80ea0d329143a89ece9c1d5f550ad50a0911 GIT binary patch literal 42016 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 literal 0 HcmV?d00001 diff --git a/Documentation/Images/AddWidget.png b/Documentation/Images/AddWidget.png new file mode 100644 index 0000000000000000000000000000000000000000..2c68c22c2c591875dc2f157eb1262673ea3fa8aa GIT binary patch literal 21703 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 literal 0 HcmV?d00001 diff --git a/Documentation/Images/DashboardConfiguration.png b/Documentation/Images/DashboardConfiguration.png new file mode 100644 index 0000000000000000000000000000000000000000..41173e94dc1e3cd26390a9e4c30a82dcd8084615 GIT binary patch literal 19432 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 z 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 literal 0 HcmV?d00001 diff --git a/Documentation/Images/DashboardPosition.png b/Documentation/Images/DashboardPosition.png new file mode 100644 index 0000000000000000000000000000000000000000..db8709a792510ff4530f523b50cbda4642f83944 GIT binary patch literal 50580 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«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 z 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 Y^fROORj$H!!0dw5l(ZgIDgG7qUwE8jxc~qF literal 0 HcmV?d00001 diff --git a/Documentation/Images/DashboardWizard.png b/Documentation/Images/DashboardWizard.png new file mode 100644 index 0000000000000000000000000000000000000000..ec0344a4802107e728db3772f76435d1d1ac970d GIT binary patch literal 23802 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�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 literal 0 HcmV?d00001 diff --git a/Documentation/Images/WidgetDelete.png b/Documentation/Images/WidgetDelete.png new file mode 100644 index 0000000000000000000000000000000000000000..9aac708d8662c121171bd025dfeefe7ffd4579d7 GIT binary patch literal 16237 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 O8`|o6YLzNaUjHAndWY@+ literal 0 HcmV?d00001 diff --git a/Documentation/Images/WidgetMove.png b/Documentation/Images/WidgetMove.png new file mode 100644 index 0000000000000000000000000000000000000000..1359332cfa2f063eb8341566640be4afccd213d0 GIT binary patch literal 8787 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 literal 0 HcmV?d00001 diff --git a/Documentation/Includes.rst.txt b/Documentation/Includes.rst.txt new file mode 100644 index 0000000..2362507 --- /dev/null +++ b/Documentation/Includes.rst.txt @@ -0,0 +1 @@ +.. You can put central messages to display on all pages here diff --git a/Documentation/Index.rst b/Documentation/Index.rst new file mode 100644 index 0000000..a20bc35 --- /dev/null +++ b/Documentation/Index.rst @@ -0,0 +1,55 @@ +.. include:: /Includes.rst.txt + +.. _start: + +=============== +TYPO3 Dashboard +=============== + +:Extension key: + dashboard + +:Package name: + typo3/cms-dashboard + +:Version: + |release| + +:Language: + en + +:Author: + TYPO3 contributors + +:License: + This document is published under the + `Open Content License `__. + +:Rendered: + |today| + +---- + +This TYPO3 backend module is used to configure and create backend widgets. + +---- + +**Table of Contents:** + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + Introduction/Index + Installation/Index + Editor/Index + Configuration/Index + Developer/Index + Widgets/Index + +.. Meta Menu + +.. toctree:: + :hidden: + + Sitemap diff --git a/Documentation/Installation/Index.rst b/Documentation/Installation/Index.rst new file mode 100644 index 0000000..e1a0dbc --- /dev/null +++ b/Documentation/Installation/Index.rst @@ -0,0 +1,54 @@ +.. include:: /Includes.rst.txt + +.. _installation: + +============ +Installation +============ + +Target group: **Administrators** + +This extension is part of the TYPO3 Core, but not installed by default. + +.. contents:: Table of contents + :local: + +.. _installation-composer: + +Installation with Composer +========================== + +Check whether you are already using the extension with: + +.. code-block:: bash + + composer show | grep dashboard + +This should either give you no result or something similar to: + +.. code-block:: none + + typo3/cms-dashboard v12.4.11 + +If it is not installed yet, use the ``composer require`` command to install +the extension: + +.. code-block:: bash + + composer require typo3/cms-dashboard + +The given version depends on the version of the TYPO3 Core you are using. + +.. _installation-no-composer: + +Installation without Composer +============================= + +In an installation without Composer, the extension is already shipped. You just have to activate it. +Head over to the extension manager and activate the extension. + +.. figure:: /Images/InstallActivate.png + :class: with-shadow + :alt: Extension manager showing Dashboard extension + + Extension manager showing Dashboard extension diff --git a/Documentation/Introduction/Index.rst b/Documentation/Introduction/Index.rst new file mode 100644 index 0000000..b647163 --- /dev/null +++ b/Documentation/Introduction/Index.rst @@ -0,0 +1,16 @@ +:navigation-title: Introduction +.. include:: /Includes.rst.txt + +.. _introduction: + +============================== +Introduction: What does it do? +============================== + +This extension provides a new TYPO3 backend module "Dashboard". +Users can create multiple dashboards visible in this module, and switch between those +dashboards. +Each of the dashboards can have multiple widgets. + +Developers are able to create new widgets. +Integrators and developers are able to register new widgets through configuration. diff --git a/Documentation/Shared/DifferenceRegistrationAndImplementation.rst.txt b/Documentation/Shared/DifferenceRegistrationAndImplementation.rst.txt new file mode 100644 index 0000000..1a6a04f --- /dev/null +++ b/Documentation/Shared/DifferenceRegistrationAndImplementation.rst.txt @@ -0,0 +1,13 @@ +.. note:: + + Difference between **registration** of widgets and **implementation** of widgets: + + Widgets provide some functionality, e.g. collect system log errors over a time span. + This functionality is provided by the implementation, a PHP class with some code. + The registration is done in :file:`Services.yaml`, + in order to create the actual widget with provided functionality. + During registration options can be set, e.g. the time span. + + Registration is documented at :ref:`register-new-widget`, + while implementation is documented at + :ref:`implement-new-widget`. diff --git a/Documentation/Sitemap.rst b/Documentation/Sitemap.rst new file mode 100644 index 0000000..c809724 --- /dev/null +++ b/Documentation/Sitemap.rst @@ -0,0 +1,11 @@ +:template: sitemap.html + +.. include:: /Includes.rst.txt + +.. _sitemap: + +======= +Sitemap +======= + +.. The sitemap.html template will insert here the page tree automatically. diff --git a/Documentation/Widgets/BarChartWidget.rst b/Documentation/Widgets/BarChartWidget.rst new file mode 100644 index 0000000..34664ae --- /dev/null +++ b/Documentation/Widgets/BarChartWidget.rst @@ -0,0 +1,73 @@ +.. include:: /Includes.rst.txt + +.. _bar-chart-widget: + +================ +Bar Chart Widget +================ + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets + +Widgets using this class will show a bar chart with the provided data. + +This kind of widgets are useful if you want to show some statistics of for example +historical data. + +.. php:class:: TYPO3\CMS\Dashboard\Widgets\BarChartWidget + +.. _bar-chart-widget-example: + +Example +------- + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Services.yaml + + services: + dashboard.widget.sysLogErrors: + class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget' + arguments: + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider' + $buttonProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider' + $options: + refreshAvailable: true + tags: + - name: dashboard.widget + identifier: 'sysLogErrors' + groupNames: 'systemInfo' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.sysLogErrors.description' + iconIdentifier: 'content-widget-chart-bar' + height: 'medium' + width: 'medium' + +.. _bar-chart-widget-options: + +Options +------- + +.. include:: Options/RefreshAvailable.rst.txt + +.. _bar-chart-widget-dependencies: + +Dependencies +------------ + +.. confval:: $dataProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface` + :name: bar-chart-widget-dataProvider + + To add data to a Bar Chart widget, you need to have a DataProvider that implements + the interface :php-short:`\TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface`. + + See :ref:`graph-widget-implementation` for further information. + +.. confval:: $buttonProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface` + :name: bar-chart-widget-buttonProvider + + Optionally you can add a button with a link to some additional data. + This button should be provided by a ButtonProvider that implements the interface + :php-short:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface`. + + See :ref:`adding-buttons` for further info and configuration options. diff --git a/Documentation/Widgets/CTAButtonWidget.rst b/Documentation/Widgets/CTAButtonWidget.rst new file mode 100644 index 0000000..af93ac0 --- /dev/null +++ b/Documentation/Widgets/CTAButtonWidget.rst @@ -0,0 +1,72 @@ +.. include:: /Includes.rst.txt + +.. _cta-button-widget: + +================= +CTA Button Widget +================= + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets +.. php:class:: TYPO3\CMS\Dashboard\Widgets\CtaWidget + +Widgets using this class will show a CTA (=Call to action) button to easily go to +a specific page or do a specific action. You can add a button to the widget by +defining a button provider. + +You can use this kind of widget to link to for example a manual or to an important +website that is used a lot by the users. + +.. _cta-button-widget-example: + +Example +------- + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Services.yaml + + services: + dashboard.widget.docGettingStarted: + class: 'TYPO3\CMS\Dashboard\Widgets\CtaWidget' + arguments: + $buttonProvider: '@dashboard.buttons.docGettingStarted' + $options: + text: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text' + tags: + - name: dashboard.widget + identifier: 'docGettingStarted' + groupNames: 'documentation' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.description' + iconIdentifier: 'content-widget-text' + height: 'small' + +.. _cta-button-widget-options: + +Options +------- + +.. include:: Options/RefreshAvailable.rst.txt + +.. confval:: text + :name: cta-button-text + :type: string + :Example: `LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.documentation.gettingStarted.text` + + Adds an optional text to the widget to give some more background information + about what a user can expect when clicking the button. + You can either enter a normal string or a translation string. + +.. _cta-button-widget-dependencies: + +Dependencies +------------ + +.. confval:: $buttonProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface` + :name: cta-button-buttonProvider + + Provides the actual button to show within the widget. + This button should be provided by a ButtonProvider that implements the interface + :php-short:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface`. + + See :ref:`adding-buttons` for further info and configuration options. diff --git a/Documentation/Widgets/DoughnutChartWidget.rst b/Documentation/Widgets/DoughnutChartWidget.rst new file mode 100644 index 0000000..6b8a533 --- /dev/null +++ b/Documentation/Widgets/DoughnutChartWidget.rst @@ -0,0 +1,70 @@ +.. include:: /Includes.rst.txt + +.. _doughnut-chart-widget: + +===================== +Doughnut Chart Widget +===================== + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets +.. php:class:: TYPO3\CMS\Dashboard\Widgets\DoughnutChartWidget + +Widgets using this class will show a doughnut chart with the provided data. + +This kind of widgets are useful if you want to show the relational proportions +between data. + +.. _doughnut-chart-widget-example: + +Example +------- + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Services.yaml + + services: + dashboard.widget.typeOfUsers: + class: 'TYPO3\CMS\Dashboard\Widgets\DoughnutChartWidget' + arguments: + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\TypeOfUsersChartDataProvider' + $options: + refreshAvailable: true + tags: + - name: dashboard.widget + identifier: 'typeOfUsers' + groupNames: 'systemInfo' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.typeOfUsers.description' + iconIdentifier: 'content-widget-chart-pie' + height: 'medium' + +.. _doughnut-chart-widget-options: + +Options +------- + +.. include:: Options/RefreshAvailable.rst.txt + +.. _doughnut-chart-widget-dependencies: + +Dependencies +------------ + +.. confval:: $dataProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface` + :name: doughnut-chart-widget-dataProvider + + To add data to a Bar Chart widget, you need to have a DataProvider that implements + the interface :php-short:`\TYPO3\CMS\Dashboard\Widgets\ChartDataProviderInterface`. + + See :ref:`graph-widget-implementation` for further information. + +.. confval:: $buttonProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface` + :name: doughnut-chart-widget-buttonProvider + + Optionally you can add a button with a link to some additional data. + This button should be provided by a ButtonProvider that implements the interface + :php-short:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface`. + + See :ref:`adding-buttons` for further info and configuration options. diff --git a/Documentation/Widgets/Index.rst b/Documentation/Widgets/Index.rst new file mode 100644 index 0000000..7c2eb21 --- /dev/null +++ b/Documentation/Widgets/Index.rst @@ -0,0 +1,17 @@ +.. include:: /Includes.rst.txt + +.. _widgets: + +======= +Widgets +======= + +The following section will provide information for all provided widgets. +For each widget an example registration will be provided, +together with all configuration options. + +.. toctree:: + :glob: + :titlesonly: + + *Widget diff --git a/Documentation/Widgets/ListWidget.rst b/Documentation/Widgets/ListWidget.rst new file mode 100644 index 0000000..67e368d --- /dev/null +++ b/Documentation/Widgets/ListWidget.rst @@ -0,0 +1,58 @@ +.. include:: /Includes.rst.txt + +.. _list-widget: + +=========== +List Widget +=========== + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets +.. php:class:: TYPO3\CMS\Dashboard\Widgets\ListWidget + +Widgets using this class will show a simple list of items provided by a data +provider. + +.. _list-widget-example: + +Example +------- + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Services.yaml + + services: + dashboard.widget.testList: + class: 'TYPO3\CMS\Dashboard\Widgets\ListWidget' + arguments: + $dataProvider: '@Vendor\Ext\Widgets\Provider\TestListWidgetDataProvider' + $options: + refreshAvailable: true + tags: + - name: dashboard.widget + identifier: 'testList' + groupNames: 'general' + title: 'List widget' + description: 'Description of widget' + iconIdentifier: 'content-widget-list' + height: 'large' + width: 'large' + +.. _list-widget-options: + +Options +------- + +.. include:: Options/RefreshAvailable.rst.txt + +.. _list-widget-dependencies: + +Dependencies +------------ + +.. confval:: $dataProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\NumberWithIconDataProviderInterface` + :name: list-widget-dataProvider + + This class should provide the items to show. + This data provider needs to implement the + :php-short:`\TYPO3\CMS\Dashboard\Widgets\NumberWithIconDataProviderInterface`. diff --git a/Documentation/Widgets/NumberWithIconWidget.rst b/Documentation/Widgets/NumberWithIconWidget.rst new file mode 100644 index 0000000..f35c107 --- /dev/null +++ b/Documentation/Widgets/NumberWithIconWidget.rst @@ -0,0 +1,84 @@ +.. include:: /Includes.rst.txt + +.. _number-widget: + +======================= +Number With Icon Widget +======================= + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets +.. php:class:: TYPO3\CMS\Dashboard\Widgets\NumberWithIconWidget + +Widgets using this class will show a widget with a number, some additional +text and an icon. + +This kind of widgets are useful if you want to show some simple stats. + +.. _number-widget-example: + +Example +------- + +.. code-block:: yaml + :caption: Excerpt from EXT:dashboard/Configuration/Services.yaml + + services: + dashboard.widget.failedLogins: + class: 'TYPO3\CMS\Dashboard\Widgets\NumberWithIconWidget' + arguments: + $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\NumberOfFailedLoginsDataProvider' + $options: + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title' + subtitle: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle' + icon: 'content-elements-login' + refreshAvailable: true + tags: + - name: dashboard.widget + identifier: 'failedLogins' + groupNames: 'systemInfo' + title: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title' + description: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.description' + iconIdentifier: 'content-widget-number' + +.. _number-widget-options: + +Options +------- + +.. include:: Options/RefreshAvailable.rst.txt + +.. confval:: title + :type: string + :name: number-widget-title + :Example: `LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.title` + + The main title that will be shown in the widget as an explanation of the shown number. + You can either enter a normal string or a translation string. + +.. confval:: subtitle + :type: string + :name: number-widget-subtitle + :Example: `LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.failedLogins.subtitle` + + The optional subtitle that will give some additional information about the number and title. + You can either enter a normal string or a translation string. + +.. confval:: icon + :type: string + :name: number-widget-icon + + The icon-identifier of the icon that should be shown in the widget. + You should register your icon with the :ref:`t3coreapi:icon`. + +.. _number-widget-dependencies: + +Dependencies +------------ + +.. confval:: $dataProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\NumberWithIconDataProviderInterface` + :name: number-widget-dataProvider + + This class should provide the number to show. + This data provider needs to implement the + :php-short:`\TYPO3\CMS\Dashboard\Widgets\NumberWithIconDataProviderInterface`. diff --git a/Documentation/Widgets/Options/RefreshAvailable.rst.txt b/Documentation/Widgets/Options/RefreshAvailable.rst.txt new file mode 100644 index 0000000..25dbadb --- /dev/null +++ b/Documentation/Widgets/Options/RefreshAvailable.rst.txt @@ -0,0 +1,9 @@ +.. confval:: refreshAvailable + :type: boolean + :default: :yaml:`false` + :noindex: + + Boolean value, either :yaml:`false` or :yaml:`true`. + + Provides a refresh button to backend users to refresh the widget. + If the option is omitted :yaml:`false` is assumed. diff --git a/Documentation/Widgets/RssWidget.rst b/Documentation/Widgets/RssWidget.rst new file mode 100644 index 0000000..be49361 --- /dev/null +++ b/Documentation/Widgets/RssWidget.rst @@ -0,0 +1,128 @@ +.. include:: /Includes.rst.txt + +.. _rss-widget: + +========== +RSS Widget +========== + +.. versionchanged:: 14.0 + The RSS widget was extended to support Atom feeds (commonly used by GitHub, + GitLab, and other platforms) + +.. php:namespace:: TYPO3\CMS\Dashboard\Widgets +.. php:class:: TYPO3\CMS\Dashboard\Widgets\RssWidget + +Widgets using this class will show a list of items of the configured RSS feed +or Atom feed. + +The "RSS Widget" supports both RSS and Atom feeds via automatic detection. + +Widget instances are fully configurable with custom labels, feed URLs, and +display limits. Each widget can be configured independently, allowing multiple +feeds of different formats on the same dashboard + +Automatic caching ensures optimal performance with configurable cache lifetimes. + +.. contents:: + +.. _rss-widget-usage: + +Usage of the RSS Widget in the dashboard +---------------------------------------- + +You can use this kind of widget to show your own RSS feed +or Atom feed. + +#. Navigate to the dashboard where you want to add the widget +#. Click "Add widget" and select the RSS widget +#. Click the settings (cog) icon to customize the widget +#. Configure the feed URL (RSS or Atom), limit, and label as needed +#. Save the configuration to apply changes + +.. _rss-widget-format: + +Feed format support +------------------- + +The RSS widget now supports both feed formats: + +**RSS Feeds:** + +Item titles + Displayed as clickable links +Publication dates + Used for sorting entries (newest first) +Descriptions + Displayed as entry content (HTML tags stripped) + +**Atom Feeds:** + +Entry titles + Displayed as clickable links +Publication dates + Used for sorting entries (newest first) +Content/Summary + Displayed as entry description (HTML tags stripped) +Author information + Name, email, and URL when provided in the feed + +.. _rss-widget-example: + +Example for RSS widget with Atom feed +------------------------------------- + +.. literalinclude:: _codesnippets/_rsswidget-services.yaml + :caption: EXT:my_extension/Configuration/Services.yaml + +.. _rss-widget-options: + +Options +------- + +.. include:: Options/RefreshAvailable.rst.txt + +The following options are available via :yaml:`services.dashboard.widget.t3news.arguments.$options`: + +.. confval:: label + :type: string + :name: rss-widget-label + + * Custom title for the widget instance + * Optional field that defaults to the widget's default title + +.. confval:: feedUrl + :type: string + :name: rss-widget-feedUrl + + Defines the URL or file providing the RSS Feed. + This is read by the widget in order to fetch entries to show. + +.. confval:: lifeTime + :type: int + :name: rss-widget-lifeTime + :Default: `43200` + + Defines how long to wait, in seconds, until fetching RSS Feed again. + +.. confval:: limit + :type: int + :name: rss-widget-limit + :Default: `5` + + Defines how many RSS items should be shown. + +.. _rss-widget-dependencies: + +Dependencies +------------ + +.. confval:: $buttonProvider + :type: :php:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface` + :name: rss-widget-buttonProvider + + Provides an optional button to show which is used to open the source of RSS data. + This button should be provided by a ButtonProvider that implements the interface + :php-short:`\TYPO3\CMS\Dashboard\Widgets\ButtonProviderInterface`. + + See :ref:`adding-buttons` for further info and configuration options. diff --git a/Documentation/Widgets/_codesnippets/_rsswidget-services.yaml b/Documentation/Widgets/_codesnippets/_rsswidget-services.yaml new file mode 100644 index 0000000..d4e6641 --- /dev/null +++ b/Documentation/Widgets/_codesnippets/_rsswidget-services.yaml @@ -0,0 +1,28 @@ +services: + + # Button provider for external link + dashboard.buttons.github_releases: + class: 'TYPO3\CMS\Dashboard\Widgets\Provider\ButtonProvider' + arguments: + $title: 'View all releases' + $link: 'https://github.com/TYPO3/typo3/releases' + $target: '_blank' + + # RSS widget with Atom feed URL + dashboard.widget.github_releases: + class: 'TYPO3\CMS\Dashboard\Widgets\RssWidget' + arguments: + $buttonProvider: '@dashboard.buttons.github_releases' + $options: + feedUrl: 'https://github.com/TYPO3/typo3/releases.atom' + lifeTime: 43200 + limit: 10 + tags: + - name: dashboard.widget + identifier: 'github_releases' + groupNames: 'general' + title: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:widgets.github_releases.title' + description: 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:widgets.github_releases.description' + iconIdentifier: 'content-widget-rss' + height: 'large' + width: 'medium' diff --git a/Documentation/guides.xml b/Documentation/guides.xml new file mode 100644 index 0000000..245196a --- /dev/null +++ b/Documentation/guides.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e6e5569 --- /dev/null +++ b/README.rst @@ -0,0 +1,10 @@ +============================= +TYPO3 extension ``dashboard`` +============================= + +This TYPO3 backend module is used to configure and create backend widgets. + +:Repository: https://github.com/typo3/typo3 +:Issues: https://forge.typo3.org/ +:Read online: https://docs.typo3.org/c/typo3/cms-dashboard/main/en-us/ +:Packagist: https://packagist.org/packages/typo3/cms-dashboard diff --git a/Resources/Private/Language/db.xlf b/Resources/Private/Language/db.xlf new file mode 100644 index 0000000..19cdaf7 --- /dev/null +++ b/Resources/Private/Language/db.xlf @@ -0,0 +1,17 @@ + + + +

+ + + Dashboard + + + Identifier + + + Title + + + + diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf new file mode 100644 index 0000000..b45f735 --- /dev/null +++ b/Resources/Private/Language/locallang.xlf @@ -0,0 +1,329 @@ + + + +
+ + + Close + + + Dashboard + + + Add widget + + + Add widget to "%s" + + + Search for dashboard widgets + + + Unfortunately no dashboard widget matches your query, please try a different one. + + + No widgets are available with your current permissions. + + + Close + + + Add widget + + + Refresh Widget + + + Widget Settings + + + Close + + + Save + + + Settings unchanged + + + Move Widget + + + Remove Widget + + + Close + + + Remove + + + Remove widget + + + Are you sure you want to remove this widget? + + + An error occurred while retrieving information for this widget. Please try again, and if the problem persists, contact your administrator. + + + Dashboard "%s" + + + My dashboard + + + Create a new dashboard with some general TYPO3 widgets on it + + + Empty dashboard + + + Your dashboard will be created without any widget on it. + + + Add dashboard + + + Close + + + Add Dashboard + + + Configure dashboard + + + Close + + + Save Dashboard + + + Delete dashboard + + + Are you sure you want to delete this dashboard? + + + Yes, delete it + + + Cancel + + + Title of dashboard + + + Getting Started with TYPO3 + + + Add a shortcut to the TYPO3 Getting Started Tutorial. + + + New to TYPO3? In our Getting Started Tutorial you will learn the basics how to use TYPO3. + + + Getting Started Tutorial + + + TypoScript Template Reference + + + Add a shortcut to the TYPO3 TypoScript Template Reference. + + + This document is a reference, used to lookup TypoScript templating. + + + TypoScript Template Reference + + + TSconfig Reference + + + Add a shortcut to the TYPO3 TSconfig Reference. + + + This document describes TSconfig and its options. + + + TSconfig Reference + + + TYPO3 news + + + Add a list of news items from the TYPO3 project + + + More TYPO3 news + + + About TYPO3 + + + Some general information about TYPO3 and the version used + + +
For further information visit
typo3.org.

TYPO3 CMS is freely available under the TYPO3-license (GNU/GPL).

You are using version %s - Copyright %s %s]]> + + + + Number of errors in system log + + + Shows the number of errors in the sys log of the last month, grouped by date + + + Show all errors in log + + + Show all user logins + + + Number of errors + + + Type of backend users + + + Shows the amount of normal- and admin-users + + + Normal users + + + Admin users + + + Failed backend logins + + + In last 24 hours + + + Information about the number of failed logins during the last 24 hours. + + + TYPO3 security advisories + + + Add a list of security advisories from the TYPO3 project + + + More TYPO3 security advisories + + + Latest changed pages + + + My latest changed pages + + + Show a list of pages where the latest changes were made + + + No history found + + + Only show my changes + + + Change + + + User + + + Not Found + + + Page + + + Changes in current selected workspace + + + Position in pagetree + + + Control + + + Show history + + + View webpage + + + Edit page content + + + Edit page properties + + + Content + + + Page + + + Latest backend logins + + + Shows the latest backend user logins. + + + No backend login history found + + + Time + + + User + + + Not found + + + Edit user + + + Number of logins to display + + + Maximum number of backend user logins to show in the widget + + + General + + + TYPO3 community + + + News + + + Documentation + + + System Information + + + Content + + + No widgets + + + There are no widgets on this dashboard yet. You can add widgets now by clicking on the button below. + + + Add a widget + + + + diff --git a/Resources/Private/Language/locallang_widget_bookmarks.xlf b/Resources/Private/Language/locallang_widget_bookmarks.xlf new file mode 100644 index 0000000..2dec443 --- /dev/null +++ b/Resources/Private/Language/locallang_widget_bookmarks.xlf @@ -0,0 +1,32 @@ + + + +
+ + + Bookmarks + + + Display your bookmarks as a dashboard widget. + + + Bookmark groups + + + Bookmark groups to be displayed. + + + Limit + + + Maximum number of bookmarks to display. + + + Show all bookmarks + + + Manage bookmarks + + + + diff --git a/Resources/Private/Language/locallang_widget_rss.xlf b/Resources/Private/Language/locallang_widget_rss.xlf new file mode 100644 index 0000000..21b63b1 --- /dev/null +++ b/Resources/Private/Language/locallang_widget_rss.xlf @@ -0,0 +1,56 @@ + + + +
+ + + RSS Feed + + + Displays the latest entries from a customizable RSS feed. + + + Label + + + Defines the label of the widget. + + + RSS Feed + + + Defines the URL or file providing the RSS Feed. This is read by the widget in order to fetch entries to show. + + + Limit + + + Defines how many RSS items should be shown. + + + Lifetime + + + Defines how long to wait, in seconds, until fetching RSS Feed again. + + + Missing feed configuration + + + Please configure a feed URL to retrieve items from. + + + Feed error + + + The feed at "%1$s" could not be parsed. + + + No items to display + + + The feed at "%1$s" does not contain any items. + + + + diff --git a/Resources/Private/Language/module.xlf b/Resources/Private/Language/module.xlf new file mode 100644 index 0000000..989019c --- /dev/null +++ b/Resources/Private/Language/module.xlf @@ -0,0 +1,17 @@ + + + +
+ + + Dashboard + + + A dashboard that can show widgets of all kind of extensions + + + Dashboard + + + + diff --git a/Resources/Private/Layouts/Widget/Widget.fluid.html b/Resources/Private/Layouts/Widget/Widget.fluid.html new file mode 100644 index 0000000..9dbe53d --- /dev/null +++ b/Resources/Private/Layouts/Widget/Widget.fluid.html @@ -0,0 +1,11 @@ + +
+ +
+ + + + + diff --git a/Resources/Private/Partials/Widget/Button.fluid.html b/Resources/Private/Partials/Widget/Button.fluid.html new file mode 100644 index 0000000..4edecdf --- /dev/null +++ b/Resources/Private/Partials/Widget/Button.fluid.html @@ -0,0 +1,7 @@ + + + {f:translate(id: button.title, default: button.title)} + + diff --git a/Resources/Private/Templates/Dashboard/Main.fluid.html b/Resources/Private/Templates/Dashboard/Main.fluid.html new file mode 100644 index 0000000..56cff9d --- /dev/null +++ b/Resources/Private/Templates/Dashboard/Main.fluid.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Resources/Private/Templates/Widget/BookmarksWidget.fluid.html b/Resources/Private/Templates/Widget/BookmarksWidget.fluid.html new file mode 100644 index 0000000..b5bf4b8 --- /dev/null +++ b/Resources/Private/Templates/Widget/BookmarksWidget.fluid.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/Resources/Private/Templates/Widget/ChartWidget.fluid.html b/Resources/Private/Templates/Widget/ChartWidget.fluid.html new file mode 100644 index 0000000..0bbe2ba --- /dev/null +++ b/Resources/Private/Templates/Widget/ChartWidget.fluid.html @@ -0,0 +1,13 @@ + + + + +
+ +
+ +
+ + + + diff --git a/Resources/Private/Templates/Widget/CtaWidget.fluid.html b/Resources/Private/Templates/Widget/CtaWidget.fluid.html new file mode 100644 index 0000000..2b3e42d --- /dev/null +++ b/Resources/Private/Templates/Widget/CtaWidget.fluid.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/Resources/Private/Templates/Widget/LatestBeLoginsWidget.html b/Resources/Private/Templates/Widget/LatestBeLoginsWidget.html new file mode 100644 index 0000000..2a909bf --- /dev/null +++ b/Resources/Private/Templates/Widget/LatestBeLoginsWidget.html @@ -0,0 +1,66 @@ + + + + + + +
+ + + + + + + + + + + + + + + + +
{f:translate(key: 'widgets.latestBeLogins.column.user', domain: 'dashboard.messages')}{f:translate(key: 'widgets.latestBeLogins.column.time', domain: 'dashboard.messages')}
+ + + + + {item.realName} + +
({item.username})
+
+
+ + {f:if(condition: item.username, then: item.username, else: '{f:translate(key: \'widgets.latestBeLogins.column.user.notFound\', domain: \'dashboard.messages\')}')} + +
+
+ {item.timestamp -> f:format.date(format: dateFormat)} +
+
+
+ +
+
+ + + +
+
+
+ {f:translate(key: 'widgets.latestBeLogins.empty', domain: 'dashboard.messages')} +
+
+
+
+
+ +
+ + + + diff --git a/Resources/Private/Templates/Widget/LatestChangedPagesWidget.fluid.html b/Resources/Private/Templates/Widget/LatestChangedPagesWidget.fluid.html new file mode 100644 index 0000000..fbb1fc3 --- /dev/null +++ b/Resources/Private/Templates/Widget/LatestChangedPagesWidget.fluid.html @@ -0,0 +1,122 @@ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.column.change')}{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.column.user')}{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.column.page')}{f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.column.control')}
+ {page.pageRecord.tstamp -> f:format.date(format: dateFormat)}
+ {f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.table.{page.history.tablename}')} +
+ + + + + {page.realName} + +
({page.userName})
+
+
+ + {f:if(condition: page.userName, then: page.userName, else: '{f:translate(key: \'LLL:EXT:dashboard/Resources/Private/Language/locallang.xlf:widgets.latestChangedPages.column.user.userNotFound\')}')} + +
+
+ + + + + + + + + + + + + + + {page.pageRecord.title} + +
{page.rootline}
+
+
+
+ + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + + +
+
+
+ +
+
+
+
+
+ +
+ diff --git a/Resources/Private/Templates/Widget/ListWidget.fluid.html b/Resources/Private/Templates/Widget/ListWidget.fluid.html new file mode 100644 index 0000000..a0c809d --- /dev/null +++ b/Resources/Private/Templates/Widget/ListWidget.fluid.html @@ -0,0 +1,17 @@ + + + + + +
    + +
  • {item}
  • +
    +
+
+ +
+ + + + diff --git a/Resources/Private/Templates/Widget/NumberWithIconWidget.fluid.html b/Resources/Private/Templates/Widget/NumberWithIconWidget.fluid.html new file mode 100644 index 0000000..863d74c --- /dev/null +++ b/Resources/Private/Templates/Widget/NumberWithIconWidget.fluid.html @@ -0,0 +1,21 @@ + + + + +
+ +
+ +
+
+
+
+ +
+
+
{number}
+
+
+ +
+ diff --git a/Resources/Private/Templates/Widget/RssWidget.fluid.html b/Resources/Private/Templates/Widget/RssWidget.fluid.html new file mode 100644 index 0000000..d964d9e --- /dev/null +++ b/Resources/Private/Templates/Widget/RssWidget.fluid.html @@ -0,0 +1,36 @@ + + + + + + +
+ + + + + + +
+ {item.title} + | {item.author.name} +

{item.description -> f:format.crop(maxCharacters: 180)}

+
+
+
+ + {f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.missingFeedConfiguration.message')} + + + {f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.feedError.message', arguments: '{0: feedUrl}')} + + + {f:translate(key: 'LLL:EXT:dashboard/Resources/Private/Language/locallang_widget_rss.xlf:widget.rss.noItems.message', arguments: '{0: feedUrl}')} + +
+ +
+ + + + diff --git a/Resources/Private/Templates/Widget/T3GeneralInformationWidget.fluid.html b/Resources/Private/Templates/Widget/T3GeneralInformationWidget.fluid.html new file mode 100644 index 0000000..3a7c2d4 --- /dev/null +++ b/Resources/Private/Templates/Widget/T3GeneralInformationWidget.fluid.html @@ -0,0 +1,27 @@ + + + + +
+
+ +

+ {f:translate(key: 'widgets.t3information.text', + extensionName: 'dashboard', + arguments: '{ + 0: currentVersion, + 1: "© {copyrightYear}", + 2: "Kasper Skårhøj" + }') -> f:format.raw()} +

+
+
+
+ +
+
+ +
+ diff --git a/Resources/Public/Css/Modal/style.css b/Resources/Public/Css/Modal/style.css new file mode 100644 index 0000000..20b6f31 --- /dev/null +++ b/Resources/Public/Css/Modal/style.css @@ -0,0 +1,24 @@ +/*! + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +.dashboard-modal-tabpanel .nav{margin:-15px -15px 0;padding:15px 15px 0} +.dashboard-modal-items{display:grid;gap:var(--typo3-spacing);grid-template-columns:repeat(2,1fr)} +.dashboard-modal-item-block{background-color:var(--typo3-component-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);box-shadow:var(--typo3-component-box-shadow);color:var(--typo3-component-color);display:flex;font-weight:400;gap:var(--typo3-spacing);grid-column:span 2;height:100%;padding:var(--typo3-component-padding-y) var(--typo3-component-padding-x);text-align:left} +.dashboard-modal-item-block>*{-webkit-user-select:none;-moz-user-select:none;user-select:none} +.dashboard-modal-item-block:hover{background-color:var(--typo3-component-hover-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-hover-border-color);color:var(--typo3-component-hover-color)} +@media screen and (min-width:768px){ +.dashboard-modal-item-block{grid-column:span 1}} +.dashboard-modal-item-checkbox{cursor:pointer;height:0;opacity:0;position:absolute;width:0} +.dashboard-modal-item-checkbox:focus~.dashboard-modal-item-block{background-color:var(--typo3-component-focus-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-focus-border-color);color:var(--typo3-component-focus-color);outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-component-focus-border-color),transparent 25%)} +.dashboard-modal-item-checkbox:checked~.dashboard-modal-item-block{background-color:var(--typo3-component-active-bg);border:var(--typo3-component-border-width) solid var(--typo3-component-active-border-color);color:var(--typo3-component-active-color)} +.dashboard-modal-item-checkbox:focus:checked~.dashboard-modal-item-block{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--typo3-component-active-border-color),transparent 25%)} +.dashboard-modal-item-title{display:block;font-weight:700} \ No newline at end of file diff --git a/Resources/Public/Css/dashboard.css b/Resources/Public/Css/dashboard.css new file mode 100644 index 0000000..a81d49a --- /dev/null +++ b/Resources/Public/Css/dashboard.css @@ -0,0 +1,84 @@ +/*! + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +.dashboard-header{--dashboard-header-bg:var(--typo3-surface-container-high);--dashboard-header-border-color:var(--typo3-surface-container-highest);background:var(--dashboard-header-bg);border-bottom:1px solid var(--dashboard-header-border-color);margin:calc(var(--module-body-padding-y)*-1) calc(var(--module-body-padding-x)*-1) var(--module-body-padding-y);padding:var(--module-body-padding-y) var(--module-body-padding-x) 0} +.dashboard-header-container{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem;justify-content:space-between} +.dashboard-loader{align-items:center;bottom:0;display:flex;justify-content:center;left:0;position:absolute;right:0;top:0} +.dashboard-tabs{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem} +.dashboard-tabs-menu{display:flex;gap:2px;list-style:none;margin:0;padding:0} +.dashboard-tab{--dashboard-header-tab-padding:1rem;--dashboard-header-tab-state-color:initial;--dashboard-header-tab-state-bg:initial;--dashboard-header-tab-border-color:color-mix(in srgb,var(--dashboard-header-tab-bg),var(--dashboard-header-tab-color) var(--typo3-border-mix));--dashboard-header-tab-color:var(--dashboard-header-tab-state-color,var(--module-color));--dashboard-header-tab-bg:var(--dashboard-header-tab-state-bg,color-mix(in srgb,var(--module-bg),var(--dashboard-header-tab-color) 5%));--dashboard-header-tab-hover-color:var(--dashboard-header-tab-color);--dashboard-header-tab-hover-bg:color-mix(in srgb,color-mix(in srgb,var(--module-bg),var(--dashboard-header-tab-color) 5%),var(--dashboard-header-tab-color) 5%);--dashboard-header-tab-focus-color:var(--typo3-state-default-focus-color);--dashboard-header-tab-focus-bg:var(--typo3-state-default-focus-bg);--dashboard-header-tab-active-color:var(--module-color);--dashboard-header-tab-active-bg:var(--module-bg);--dashboard-header-tab-border-radius:var(--typo3-component-border-radius);border:1px solid var(--dashboard-header-tab-border-color);border-radius:var(--dashboard-header-tab-border-radius) var(--dashboard-header-tab-border-radius) 0 0;-webkit-margin-after:-1px;background:var(--dashboard-header-tab-bg);color:var(--dashboard-header-tab-color);display:inline-block;margin-block-end:-1px;outline-offset:-1px;padding:calc(var(--dashboard-header-tab-padding)/1.5) var(--dashboard-header-tab-padding)} +.dashboard-tab:focus,.dashboard-tab:hover{--dashboard-header-tab-state-bg:var(--dashboard-header-tab-hover-bg);isolation:isolate;text-decoration:none} +.dashboard-tab:focus-visible{border-radius:var(--dashboard-header-tab-border-radius);outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--dashboard-header-tab-color),transparent 50%)} +.dashboard-tab--active{--dashboard-header-tab-state-color:var(--dashboard-header-tab-active-color)!important;--dashboard-header-tab-state-bg:var(--dashboard-header-tab-active-bg)!important;border-bottom-color:var(--dashboard-header-tab-state-bg)} +.dashboard-tab:has(+.btn-dashboard-add-tab){-webkit-margin-end:.25rem;margin-inline-end:.25rem} +.dashboard-empty{position:relative} +.dashboard-empty-content{background-color:var(--typo3-component-bg);border:var(--typo3-component-border-width) dashed var(--typo3-component-border-color);border-radius:var(--typo3-component-border-radius);padding:2.5em;text-align:center} +.dashboard-empty-content h3{font-size:1.5em;margin-bottom:.5em} +.dashboard-empty-content p{font-size:1.25em;margin-bottom:1em} +.dashboard-empty-content>:first-child{margin-top:0} +.dashboard-empty-content>:last-child{margin-bottom:0} +typo3-dashboard{display:block;width:100%} +.dashboard-configuration{margin:.25rem 0} +.dashboard-container{max-width:1600px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto} +.dashboard-dragging-container>typo3-dashboard-widget{position:fixed} +.dashboard-grid{--gutter:20px;--columns:1;--row-height:200px;display:grid;grid-template-columns:repeat(var(--columns),1fr);grid-gap:var(--gutter);grid-auto-rows:var(--row-height)} +.dashboard-item{--dashboard-item-border-color:transparent;--dashboard-item-border-radius:var(--typo3-component-border-radius);--dashboard-item-bg:transparent;--dashboard-item-active-border-color:var(--typo3-state-primary-border-color);--dashboard-item-active-bg:color-mix(in srgb,transparent,var(--typo3-state-primary-bg) 10%);--col-start:1;--col-span:1;--row-start:1;--row-span:1;background-color:var(--dashboard-item-bg);border-radius:var(--dashboard-item-border-radius);contain:inline-size;grid-column:var(--col-start)/span var(--col-span);grid-row:var(--row-start)/span var(--row-span);outline:1px dashed var(--dashboard-item-border-color);outline-offset:-1px;transition:outline .15s ease-out,background-color .15s ease-out} +@media (prefers-reduced-motion:reduce){ +.dashboard-item{transition:none}} +.dashboard-item-content{height:100%;position:relative;width:100%} +.dashboard-item-dragging{--dashboard-item-border-color:var(--dashboard-item-active-border-color);--dashboard-item-bg:var(--dashboard-item-active-bg)} +.btn-dashboard-add-widget{--typo3-btn-padding-y:1rem;--typo3-btn-padding-x:1rem;bottom:24px;box-shadow:var(--typo3-component-box-shadow-flyout);inset-inline-end:24px;position:fixed;z-index:2} +.widget{--widget-padding:1.25rem;--widget-color:var(--typo3-component-color);--widget-bg:var(--typo3-component-bg);--widget-border-width:1px;--widget-border-radius:var(--typo3-component-border-radius);--widget-border-color:color-mix(in srgb,var(--widget-bg),var(--widget-color) var(--typo3-border-mix));--widget-box-shadow:var(--typo3-component-box-shadow);--widget-title-font-size:1rem;--widget-title-line-height:1.25;--widget-title-font-family:var(--typo3-header-font-family);--widget-title-color-state:initial;--widget-title-bg-state:initial;--widget-title-color:var(--widget-color);--widget-title-bg:var(--widget-bg);--widget-action-size:28px;background-color:var(--widget-bg);border:var(--widget-border-width) solid var(--widget-border-color);border-radius:var(--widget-border-radius);box-shadow:var(--widget-box-shadow);color:var(--widget-color);display:flex;flex-direction:column;height:100%;overflow:hidden} +.widget:focus-within .widget-action,.widget:hover .widget-action{opacity:.75} +.widget.widget-selected{--widget-border-color:var(--typo3-state-primary-border-color)} +.widget-loader{flex-grow:1;justify-content:center} +.widget-header,.widget-loader{align-items:center;display:flex} +.widget-header{border-bottom:var(--typo3-component-border-width) solid var(--typo3-component-border-color);cursor:grab;gap:var(--typo3-spacing);justify-content:space-between;padding:var(--typo3-component-padding-y) var(--typo3-component-padding-x)} +.widget-title{display:block;font-family:var(--typo3-header-font-family);font-size:.875rem;font-weight:700;line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.widget-content{display:flex;flex-direction:column;height:100%;-webkit-user-select:text;-moz-user-select:text;user-select:text} +.widget-content-title{background-color:var(--widget-title-bg-state,var(--widget-title-bg));color:var(--widget-title-color-state,var(--widget-title-color));padding:calc(var(--widget-padding)/2) var(--widget-padding);-webkit-padding-end:calc(var(--widget-action-size)*3 + var(--widget-padding));border-bottom:var(--widget-border-width) solid var(--widget-border-color);font-family:var(--widget-title-font-family);font-size:var(--widget-title-font-size);font-weight:700;line-height:var(--widget-title-line-height);padding-inline-end:calc(var(--widget-action-size)*3 + var(--widget-padding))} +.widget-content-title span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.widget-content-title h2{margin-bottom:0} +.widget-content-main{flex-basis:0;flex-grow:1;overflow-y:auto;padding:var(--widget-padding)} +.widget-content-footer{padding:var(--widget-padding);padding-top:0} +.widget-actions{color:var(--widget-title-color-state,var(--widget-title-color));display:flex;inset-inline-end:calc(var(--widget-padding)/2)} +.widget-action{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:none;border-radius:4px;color:inherit;height:var(--widget-action-size);opacity:.5;position:relative;text-align:center;transition:opacity .2s ease-in-out;width:var(--widget-action-size)} +.widget-action:focus,.widget-action:hover{opacity:1!important} +.widget-action:focus,.widget-action:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,currentColor,transparent 25%)!important} +.widget-action .icon{inset-inline-start:50%;position:absolute;top:50%;transform:translate(calc(-50%*var(--typo3-position-modifier)),-50%)} +.widget-action-move{cursor:grab} +.widget-chart{height:100%;width:100%} +.widget-edit{text-align:center;width:45px} +.widget-identifier-sys_note_default{--widget-title-bg:var(--typo3-state-default-bg);--widget-title-color:var(--typo3-state-default-color)} +.widget-identifier-sys_note_todos{--widget-title-bg:var(--typo3-state-success-bg);--widget-title-color:var(--typo3-state-success-color)} +.widget-identifier-sys_note_instructions{--widget-title-bg:var(--typo3-state-info-bg);--widget-title-color:var(--typo3-state-info-color)} +.widget-identifier-sys_note_notes{--widget-title-bg:var(--typo3-state-notice-bg);--widget-title-color:var(--typo3-state-notice-color)} +.widget-identifier-sys_note_template{--widget-title-bg:var(--typo3-state-warning-bg);--widget-title-color:var(--typo3-state-warning-color)} +.widget-table-wrapper{margin:calc(var(--widget-padding)*-1)} +.widget-table{margin:0} +.widget-table td:first-child,.widget-table th:first-child{-webkit-padding-start:var(--widget-padding);padding-inline-start:var(--widget-padding)} +.widget-table td:last-child,.widget-table th:last-child{-webkit-padding-end:var(--widget-padding);padding-inline-end:var(--widget-padding)} +.widget-table tbody:last-child tr:last-child td,.widget-table tbody:last-child tr:last-child th{border-bottom:none} +.widget-cta{--widget-cta-padding:8px;--widget-cta-bg:var(--typo3-state-secondary-bg);--widget-cta-color:var(--typo3-state-secondary-color);--widget-cta-hover-bg:var(--typo3-state-primary-hover-bg);--widget-cta-hover-color:var(--typo3-state-primary-hover-color);--widget-cta-focus-bg:var(--typo3-state-primary-focus-bg);--widget-cta-focus-color:var(--typo3-state-primary-focus-color);--widget-cta-border-radius:var(--typo3-component-border-radius);align-items:center;background-color:var(--widget-cta-bg);border-radius:var(--widget-cta-border-radius);color:var(--widget-cta-color);display:flex;justify-content:center;outline-offset:0;padding:var(--widget-cta-padding)} +.widget-cta:hover{--widget-cta-color:var(--widget-cta-hover-color);--widget-cta-bg:var(--widget-cta-hover-bg);text-decoration:none} +.widget-cta:focus{--widget-cta-color:var(--widget-cta-focus-color);--widget-cta-bg:var(--widget-cta-focus-bg);text-decoration:none} +.widget-cta:focus-visible{outline:var(--typo3-outline-width) var(--typo3-outline-style) color-mix(in srgb,var(--widget-cta-bg),transparent 25%)} +.widget-cta-icon{align-items:center;display:flex;height:18px;justify-content:center;width:18px;-webkit-margin-end:12px;color:var(--widget-cta-color);margin-inline-end:12px} +.widget-number{align-items:center;display:flex;height:100%} +.widget-number-icon{align-items:center;display:flex;width:42px;-webkit-margin-end:20px;margin-inline-end:20px} +.widget-number-content{display:flex;flex-direction:column;justify-content:center} +.widget-number-title{font-size:1rem;line-height:1.3;margin-bottom:5px} +.widget-number-number{font-size:24px;font-weight:900;line-height:1.3} +.widget-listgroup{--typo3-list-group-border-radius:0;--typo3-outline-width:2px;margin:calc(var(--widget-padding)*-1)} +.widget-listgroup .list-group-item{-webkit-border-start:0;border-inline-start:0;-webkit-border-end:0;border-inline-end:0;outline-offset:calc(var(--typo3-outline-width)*-1)} +.widget-listgroup .list-group-item:first-child{-webkit-border-before:0;border-block-start:0} +.widget-listgroup .list-group-item:last-child{-webkit-border-after:0;border-block-end:0} \ No newline at end of file diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg new file mode 100644 index 0000000..8dae94f --- /dev/null +++ b/Resources/Public/Icons/Extension.svg @@ -0,0 +1 @@ + diff --git a/Resources/Public/JavaScript/Contrib/chartjs.js b/Resources/Public/JavaScript/Contrib/chartjs.js new file mode 100644 index 0000000..25b8490 --- /dev/null +++ b/Resources/Public/JavaScript/Contrib/chartjs.js @@ -0,0 +1,29 @@ +function ae(i){return i+.5|0}var yt=(i,t,e)=>Math.max(Math.min(i,e),t);function ne(i){return yt(ae(i*2.55),0,255)}function vt(i){return yt(ae(i*255),0,255)}function ft(i){return yt(ae(i/2.55)/100,0,1)}function Ls(i){return yt(ae(i*100),0,100)}var et={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},fi=[..."0123456789ABCDEF"],Uo=i=>fi[i&15],Xo=i=>fi[(i&240)>>4]+fi[i&15],De=i=>(i&240)>>4===(i&15),Ko=i=>De(i.r)&&De(i.g)&&De(i.b)&&De(i.a);function qo(i){var t=i.length,e;return i[0]==="#"&&(t===4||t===5?e={r:255&et[i[1]]*17,g:255&et[i[2]]*17,b:255&et[i[3]]*17,a:t===5?et[i[4]]*17:255}:(t===7||t===9)&&(e={r:et[i[1]]<<4|et[i[2]],g:et[i[3]]<<4|et[i[4]],b:et[i[5]]<<4|et[i[6]],a:t===9?et[i[7]]<<4|et[i[8]]:255})),e}var Go=(i,t)=>i<255?t(i):"";function Zo(i){var t=Ko(i)?Uo:Xo;return i?"#"+t(i.r)+t(i.g)+t(i.b)+Go(i.a,t):void 0}var Jo=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Is(i,t,e){let s=t*Math.min(e,1-e),n=(o,a=(o+i/30)%12)=>e-s*Math.max(Math.min(a-3,9-a,1),-1);return[n(0),n(8),n(4)]}function Qo(i,t,e){let s=(n,o=(n+i/60)%6)=>e-e*t*Math.max(Math.min(o,4-o,1),0);return[s(5),s(3),s(1)]}function ta(i,t,e){let s=Is(i,1,.5),n;for(t+e>1&&(n=1/(t+e),t*=n,e*=n),n=0;n<3;n++)s[n]*=1-t-e,s[n]+=t;return s}function ea(i,t,e,s,n){return i===n?(t-e)/s+(t.5?h/(2-o-a):h/(o+a),l=ea(e,s,n,h,o),l=l*60+.5),[l|0,c||0,r]}function pi(i,t,e,s){return(Array.isArray(t)?i(t[0],t[1],t[2]):i(t,e,s)).map(vt)}function mi(i,t,e){return pi(Is,i,t,e)}function ia(i,t,e){return pi(ta,i,t,e)}function sa(i,t,e){return pi(Qo,i,t,e)}function zs(i){return(i%360+360)%360}function na(i){let t=Jo.exec(i),e=255,s;if(!t)return;t[5]!==s&&(e=t[6]?ne(+t[5]):vt(+t[5]));let n=zs(+t[2]),o=+t[3]/100,a=+t[4]/100;return t[1]==="hwb"?s=ia(n,o,a):t[1]==="hsv"?s=sa(n,o,a):s=mi(n,o,a),{r:s[0],g:s[1],b:s[2],a:e}}function oa(i,t){var e=gi(i);e[0]=zs(e[0]+t),e=mi(e),i.r=e[0],i.g=e[1],i.b=e[2]}function aa(i){if(!i)return;let t=gi(i),e=t[0],s=Ls(t[1]),n=Ls(t[2]);return i.a<255?`hsla(${e}, ${s}%, ${n}%, ${ft(i.a)})`:`hsl(${e}, ${s}%, ${n}%)`}var Rs={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Es={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function ra(){let i={},t=Object.keys(Es),e=Object.keys(Rs),s,n,o,a,r;for(s=0;s>16&255,o>>8&255,o&255]}return i}var Ce;function la(i){Ce||(Ce=ra(),Ce.transparent=[0,0,0,0]);let t=Ce[i.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var ca=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function ha(i){let t=ca.exec(i),e=255,s,n,o;if(t){if(t[7]!==s){let a=+t[7];e=t[8]?ne(a):yt(a*255,0,255)}return s=+t[1],n=+t[3],o=+t[5],s=255&(t[2]?ne(s):yt(s,0,255)),n=255&(t[4]?ne(n):yt(n,0,255)),o=255&(t[6]?ne(o):yt(o,0,255)),{r:s,g:n,b:o,a:e}}}function da(i){return i&&(i.a<255?`rgba(${i.r}, ${i.g}, ${i.b}, ${ft(i.a)})`:`rgb(${i.r}, ${i.g}, ${i.b})`)}var ui=i=>i<=.0031308?i*12.92:Math.pow(i,1/2.4)*1.055-.055,Nt=i=>i<=.04045?i/12.92:Math.pow((i+.055)/1.055,2.4);function ua(i,t,e){let s=Nt(ft(i.r)),n=Nt(ft(i.g)),o=Nt(ft(i.b));return{r:vt(ui(s+e*(Nt(ft(t.r))-s))),g:vt(ui(n+e*(Nt(ft(t.g))-n))),b:vt(ui(o+e*(Nt(ft(t.b))-o))),a:i.a+e*(t.a-i.a)}}function Oe(i,t,e){if(i){let s=gi(i);s[t]=Math.max(0,Math.min(s[t]+s[t]*e,t===0?360:1)),s=mi(s),i.r=s[0],i.g=s[1],i.b=s[2]}}function Bs(i,t){return i&&Object.assign(t||{},i)}function Fs(i){var t={r:0,g:0,b:0,a:255};return Array.isArray(i)?i.length>=3&&(t={r:i[0],g:i[1],b:i[2],a:255},i.length>3&&(t.a=vt(i[3]))):(t=Bs(i,{r:0,g:0,b:0,a:1}),t.a=vt(t.a)),t}function fa(i){return i.charAt(0)==="r"?ha(i):na(i)}var oe=class i{constructor(t){if(t instanceof i)return t;let e=typeof t,s;e==="object"?s=Fs(t):e==="string"&&(s=qo(t)||la(t)||fa(t)),this._rgb=s,this._valid=!!s}get valid(){return this._valid}get rgb(){var t=Bs(this._rgb);return t&&(t.a=ft(t.a)),t}set rgb(t){this._rgb=Fs(t)}rgbString(){return this._valid?da(this._rgb):void 0}hexString(){return this._valid?Zo(this._rgb):void 0}hslString(){return this._valid?aa(this._rgb):void 0}mix(t,e){if(t){let s=this.rgb,n=t.rgb,o,a=e===o?.5:e,r=2*a-1,l=s.a-n.a,c=((r*l===-1?r:(r+l)/(1+r*l))+1)/2;o=1-c,s.r=255&c*s.r+o*n.r+.5,s.g=255&c*s.g+o*n.g+.5,s.b=255&c*s.b+o*n.b+.5,s.a=a*s.a+(1-a)*n.a,this.rgb=s}return this}interpolate(t,e){return t&&(this._rgb=ua(this._rgb,t._rgb,e)),this}clone(){return new i(this.rgb)}alpha(t){return this._rgb.a=vt(t),this}clearer(t){let e=this._rgb;return e.a*=1-t,this}greyscale(){let t=this._rgb,e=ae(t.r*.3+t.g*.59+t.b*.11);return t.r=t.g=t.b=e,this}opaquer(t){let e=this._rgb;return e.a*=1+t,this}negate(){let t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Oe(this._rgb,2,t),this}darken(t){return Oe(this._rgb,2,-t),this}saturate(t){return Oe(this._rgb,1,t),this}desaturate(t){return Oe(this._rgb,1,-t),this}rotate(t){return oa(this._rgb,t),this}};function lt(){}var qs=(()=>{let i=0;return()=>i++})();function T(i){return i===null||typeof i>"u"}function F(i){if(Array.isArray&&Array.isArray(i))return!0;let t=Object.prototype.toString.call(i);return t.slice(0,7)==="[object"&&t.slice(-6)==="Array]"}function O(i){return i!==null&&Object.prototype.toString.call(i)==="[object Object]"}function V(i){return(typeof i=="number"||i instanceof Number)&&isFinite(+i)}function Z(i,t){return V(i)?i:t}function P(i,t){return typeof i>"u"?t:i}var Gs=(i,t)=>typeof i=="string"&&i.endsWith("%")?parseFloat(i)/100:+i/t,yi=(i,t)=>typeof i=="string"&&i.endsWith("%")?parseFloat(i)/100*t:+i;function E(i,t,e){if(i&&typeof i.call=="function")return i.apply(e,t)}function L(i,t,e,s){let n,o,a;if(F(i))if(o=i.length,s)for(n=o-1;n>=0;n--)t.call(e,i[n],n);else for(n=0;ni,x:i=>i.x,y:i=>i.y};function ma(i){let t=i.split("."),e=[],s="";for(let n of t)s+=n,s.endsWith("\\")?s=s.slice(0,-1)+".":(e.push(s),s="");return e}function ba(i){let t=ma(i);return e=>{for(let s of t){if(s==="")break;e=e&&e[s]}return e}}function mt(i,t){return(Vs[t]||(Vs[t]=ba(t)))(i)}function ze(i){return i.charAt(0).toUpperCase()+i.slice(1)}var Ut=i=>typeof i<"u",gt=i=>typeof i=="function",vi=(i,t)=>{if(i.size!==t.size)return!1;for(let e of i)if(!t.has(e))return!1;return!0};function Js(i){return i.type==="mouseup"||i.type==="click"||i.type==="contextmenu"}var I=Math.PI,z=2*I,xa=z+I,Ee=Number.POSITIVE_INFINITY,_a=I/180,N=I/2,Tt=I/4,Ws=I*2/3,pt=Math.log10,nt=Math.sign;function Xt(i,t,e){return Math.abs(i-t)n-o).pop(),t}function Et(i){return!isNaN(parseFloat(i))&&isFinite(i)}function tn(i,t){let e=Math.round(i);return e-t<=i&&e+t>=i}function ki(i,t,e){let s,n,o;for(s=0,n=i.length;sl&&c=Math.min(t,e)-s&&i<=Math.max(t,e)+s}function Ve(i,t,e){e=e||(a=>i[a]1;)o=n+s>>1,e(o)?n=o:s=o;return{lo:n,hi:s}}var at=(i,t,e,s)=>Ve(i,e,s?n=>{let o=i[n][t];return oi[n][t]Ve(i,e,s=>i[s][t]>=e);function nn(i,t,e){let s=0,n=i.length;for(;ss&&i[n-1]>e;)n--;return s>0||n{let s="_onData"+ze(e),n=i[e];Object.defineProperty(i,e,{configurable:!0,enumerable:!1,value(...o){let a=n.apply(this,o);return i._chartjs.listeners.forEach(r=>{typeof r[s]=="function"&&r[s](...o)}),a}})})}function Pi(i,t){let e=i._chartjs;if(!e)return;let s=e.listeners,n=s.indexOf(t);n!==-1&&s.splice(n,1),!(s.length>0)&&(on.forEach(o=>{delete i[o]}),delete i._chartjs)}function Di(i){let t=new Set(i);return t.size===i.length?i:Array.from(t)}var Ci=function(){return typeof window>"u"?function(i){return i()}:window.requestAnimationFrame}();function Oi(i,t){let e=[],s=!1;return function(...n){e=n,s||(s=!0,Ci.call(window,()=>{s=!1,i.apply(t,e)}))}}function rn(i,t){let e;return function(...s){return t?(clearTimeout(e),e=setTimeout(i,t,s)):i.apply(this,s),t}}var We=i=>i==="start"?"left":i==="end"?"right":"center",U=(i,t,e)=>i==="start"?t:i==="end"?e:(t+e)/2,ln=(i,t,e,s)=>i===(s?"left":"right")?e:i==="center"?(t+e)/2:t;function Ai(i,t,e){let s=t.length,n=0,o=s;if(i._sorted){let{iScale:a,_parsed:r}=i,l=a.axis,{min:c,max:h,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=$(Math.min(at(r,l,c).lo,e?s:at(t,l,a.getPixelForValue(c)).lo),0,s-1)),u?o=$(Math.max(at(r,a.axis,h,!0).hi+1,e?0:at(t,l,a.getPixelForValue(h),!0).hi+1),n,s)-n:o=s-n}return{start:n,count:o}}function Ti(i){let{xScale:t,yScale:e,_scaleRanges:s}=i,n={xmin:t.min,xmax:t.max,ymin:e.min,ymax:e.max};if(!s)return i._scaleRanges=n,!0;let o=s.xmin!==t.min||s.xmax!==t.max||s.ymin!==e.min||s.ymax!==e.max;return Object.assign(s,n),o}var Ae=i=>i===0||i===1,Ns=(i,t,e)=>-(Math.pow(2,10*(i-=1))*Math.sin((i-t)*z/e)),Hs=(i,t,e)=>Math.pow(2,-10*i)*Math.sin((i-t)*z/e)+1,Ht={linear:i=>i,easeInQuad:i=>i*i,easeOutQuad:i=>-i*(i-2),easeInOutQuad:i=>(i/=.5)<1?.5*i*i:-.5*(--i*(i-2)-1),easeInCubic:i=>i*i*i,easeOutCubic:i=>(i-=1)*i*i+1,easeInOutCubic:i=>(i/=.5)<1?.5*i*i*i:.5*((i-=2)*i*i+2),easeInQuart:i=>i*i*i*i,easeOutQuart:i=>-((i-=1)*i*i*i-1),easeInOutQuart:i=>(i/=.5)<1?.5*i*i*i*i:-.5*((i-=2)*i*i*i-2),easeInQuint:i=>i*i*i*i*i,easeOutQuint:i=>(i-=1)*i*i*i*i+1,easeInOutQuint:i=>(i/=.5)<1?.5*i*i*i*i*i:.5*((i-=2)*i*i*i*i+2),easeInSine:i=>-Math.cos(i*N)+1,easeOutSine:i=>Math.sin(i*N),easeInOutSine:i=>-.5*(Math.cos(I*i)-1),easeInExpo:i=>i===0?0:Math.pow(2,10*(i-1)),easeOutExpo:i=>i===1?1:-Math.pow(2,-10*i)+1,easeInOutExpo:i=>Ae(i)?i:i<.5?.5*Math.pow(2,10*(i*2-1)):.5*(-Math.pow(2,-10*(i*2-1))+2),easeInCirc:i=>i>=1?i:-(Math.sqrt(1-i*i)-1),easeOutCirc:i=>Math.sqrt(1-(i-=1)*i),easeInOutCirc:i=>(i/=.5)<1?-.5*(Math.sqrt(1-i*i)-1):.5*(Math.sqrt(1-(i-=2)*i)+1),easeInElastic:i=>Ae(i)?i:Ns(i,.075,.3),easeOutElastic:i=>Ae(i)?i:Hs(i,.075,.3),easeInOutElastic(i){return Ae(i)?i:i<.5?.5*Ns(i*2,.1125,.45):.5+.5*Hs(i*2-1,.1125,.45)},easeInBack(i){return i*i*((1.70158+1)*i-1.70158)},easeOutBack(i){return(i-=1)*i*((1.70158+1)*i+1.70158)+1},easeInOutBack(i){let t=1.70158;return(i/=.5)<1?.5*(i*i*(((t*=1.525)+1)*i-t)):.5*((i-=2)*i*(((t*=1.525)+1)*i+t)+2)},easeInBounce:i=>1-Ht.easeOutBounce(1-i),easeOutBounce(i){return i<1/2.75?7.5625*i*i:i<2/2.75?7.5625*(i-=1.5/2.75)*i+.75:i<2.5/2.75?7.5625*(i-=2.25/2.75)*i+.9375:7.5625*(i-=2.625/2.75)*i+.984375},easeInOutBounce:i=>i<.5?Ht.easeInBounce(i*2)*.5:Ht.easeOutBounce(i*2-1)*.5+.5};function Li(i){if(i&&typeof i=="object"){let t=i.toString();return t==="[object CanvasPattern]"||t==="[object CanvasGradient]"}return!1}function Ri(i){return Li(i)?i:new oe(i)}function bi(i){return Li(i)?i:new oe(i).saturate(.5).darken(.1).hexString()}var va=["x","y","borderWidth","radius","tension"],Ma=["color","borderColor","backgroundColor"];function ka(i){i.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),i.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>t!=="onProgress"&&t!=="onComplete"&&t!=="fn"}),i.set("animations",{colors:{type:"color",properties:Ma},numbers:{type:"number",properties:va}}),i.describe("animations",{_fallback:"animation"}),i.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>t|0}}}})}function wa(i){i.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var js=new Map;function Sa(i,t){t=t||{};let e=i+JSON.stringify(t),s=js.get(e);return s||(s=new Intl.NumberFormat(i,t),js.set(e,s)),s}function qt(i,t,e){return Sa(t,e).format(i)}var cn={values(i){return F(i)?i:""+i},numeric(i,t,e){if(i===0)return"0";let s=this.chart.options.locale,n,o=i;if(e.length>1){let c=Math.max(Math.abs(e[0].value),Math.abs(e[e.length-1].value));(c<1e-4||c>1e15)&&(n="scientific"),o=Pa(i,e)}let a=pt(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),qt(i,s,l)},logarithmic(i,t,e){if(i===0)return"0";let s=e[t].significand||i/Math.pow(10,Math.floor(pt(i)));return[1,2,3,5,10,15].includes(s)||t>.8*e.length?cn.numeric.call(this,i,t,e):""}};function Pa(i,t){let e=t.length>3?t[2].value-t[1].value:t[1].value-t[0].value;return Math.abs(e)>=1&&i!==Math.floor(i)&&(e=i-Math.floor(i)),e}var he={formatters:cn};function Da(i){i.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:he.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),i.route("scale.ticks","color","","color"),i.route("scale.grid","color","","borderColor"),i.route("scale.border","color","","borderColor"),i.route("scale.title","color","","color"),i.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&t!=="callback"&&t!=="parser",_indexable:t=>t!=="borderDash"&&t!=="tickBorderDash"&&t!=="dash"}),i.describe("scales",{_fallback:"scale"}),i.describe("scale.ticks",{_scriptable:t=>t!=="backdropPadding"&&t!=="callback",_indexable:t=>t!=="backdropPadding"})}var kt=Object.create(null),Ne=Object.create(null);function re(i,t){if(!t)return i;let e=t.split(".");for(let s=0,n=e.length;ss.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(s,n)=>bi(n.backgroundColor),this.hoverBorderColor=(s,n)=>bi(n.borderColor),this.hoverColor=(s,n)=>bi(n.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return xi(this,t,e)}get(t){return re(this,t)}describe(t,e){return xi(Ne,t,e)}override(t,e){return xi(kt,t,e)}route(t,e,s,n){let o=re(this,t),a=re(this,s),r="_"+e;Object.defineProperties(o,{[r]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){let l=this[r],c=a[n];return O(l)?Object.assign({},c,l):P(l,c)},set(l){this[r]=l}}})}apply(t){t.forEach(e=>e(this))}},W=new _i({_scriptable:i=>!i.startsWith("on"),_indexable:i=>i!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[ka,wa,Da]);function Ca(i){return!i||T(i.size)||T(i.family)?null:(i.style?i.style+" ":"")+(i.weight?i.weight+" ":"")+i.size+"px "+i.family}function le(i,t,e,s,n){let o=t[n];return o||(o=t[n]=i.measureText(n).width,e.push(n)),o>s&&(s=o),s}function hn(i,t,e,s){s=s||{};let n=s.data=s.data||{},o=s.garbageCollect=s.garbageCollect||[];s.font!==t&&(n=s.data={},o=s.garbageCollect=[],s.font=t),i.save(),i.font=t;let a=0,r=e.length,l,c,h,d,u;for(l=0;le.length){for(l=0;l0&&i.stroke()}}function rt(i,t,e){return e=e||.5,!t||i&&i.x>t.left-e&&i.xt.top-e&&i.y0&&o.strokeColor!=="",l,c;for(i.save(),i.font=n.string,Oa(i,o),l=0;l+i||0;function je(i,t){let e={},s=O(t),n=s?Object.keys(t):t,o=O(i)?s?a=>P(i[a],i[t[a]]):a=>i[a]:()=>i;for(let a of n)e[a]=Fa(o(a));return e}function Ii(i){return je(i,{top:"y",right:"x",bottom:"y",left:"x"})}function Pt(i){return je(i,["topLeft","topRight","bottomLeft","bottomRight"])}function X(i){let t=Ii(i);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function j(i,t){i=i||{},t=t||W.font;let e=P(i.size,t.size);typeof e=="string"&&(e=parseInt(e,10));let s=P(i.style,t.style);s&&!(""+s).match(Ra)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);let n={family:P(i.family,t.family),lineHeight:Ea(P(i.lineHeight,t.lineHeight),e),size:e,style:s,weight:P(i.weight,t.weight),string:""};return n.string=Ca(n),n}function Zt(i,t,e,s){let n=!0,o,a,r;for(o=0,a=i.length;oe&&r===0?0:r+l;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function bt(i,t){return Object.assign(Object.create(i),t)}function $e(i,t=[""],e,s,n=()=>i[0]){let o=e||i;typeof s>"u"&&(s=mn("_fallback",i));let a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:i,_rootScopes:o,_fallback:s,_getTarget:n,override:r=>$e([r,...i],t,o,s)};return new Proxy(a,{deleteProperty(r,l){return delete r[l],delete r._keys,delete i[0][l],!0},get(r,l){return gn(r,l,()=>ja(l,t,i,r))},getOwnPropertyDescriptor(r,l){return Reflect.getOwnPropertyDescriptor(r._scopes[0],l)},getPrototypeOf(){return Reflect.getPrototypeOf(i[0])},has(r,l){return Ys(r).includes(l)},ownKeys(r){return Ys(r)},set(r,l,c){let h=r._storage||(r._storage=n());return r[l]=h[l]=c,delete r._keys,!0}})}function Rt(i,t,e,s){let n={_cacheable:!1,_proxy:i,_context:t,_subProxy:e,_stack:new Set,_descriptors:zi(i,s),setContext:o=>Rt(i,o,e,s),override:o=>Rt(i.override(o),t,e,s)};return new Proxy(n,{deleteProperty(o,a){return delete o[a],delete i[a],!0},get(o,a,r){return gn(o,a,()=>za(o,a,r))},getOwnPropertyDescriptor(o,a){return o._descriptors.allKeys?Reflect.has(i,a)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(i,a)},getPrototypeOf(){return Reflect.getPrototypeOf(i)},has(o,a){return Reflect.has(i,a)},ownKeys(){return Reflect.ownKeys(i)},set(o,a,r){return i[a]=r,delete o[a],!0}})}function zi(i,t={scriptable:!0,indexable:!0}){let{_scriptable:e=t.scriptable,_indexable:s=t.indexable,_allKeys:n=t.allKeys}=i;return{allKeys:n,scriptable:e,indexable:s,isScriptable:gt(e)?e:()=>e,isIndexable:gt(s)?s:()=>s}}var Ia=(i,t)=>i?i+ze(t):t,Bi=(i,t)=>O(t)&&i!=="adapters"&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function gn(i,t,e){if(Object.prototype.hasOwnProperty.call(i,t))return i[t];let s=e();return i[t]=s,s}function za(i,t,e){let{_proxy:s,_context:n,_subProxy:o,_descriptors:a}=i,r=s[t];return gt(r)&&a.isScriptable(t)&&(r=Ba(t,r,i,e)),F(r)&&r.length&&(r=Va(t,r,i,a.isIndexable)),Bi(t,r)&&(r=Rt(r,n,o&&o[t],a)),r}function Ba(i,t,e,s){let{_proxy:n,_context:o,_subProxy:a,_stack:r}=e;if(r.has(i))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+i);r.add(i);let l=t(o,a||s);return r.delete(i),Bi(i,l)&&(l=Vi(n._scopes,n,i,l)),l}function Va(i,t,e,s){let{_proxy:n,_context:o,_subProxy:a,_descriptors:r}=e;if(typeof o.index<"u"&&s(i))return t[o.index%t.length];if(O(t[0])){let l=t,c=n._scopes.filter(h=>h!==l);t=[];for(let h of l){let d=Vi(c,n,i,h);t.push(Rt(d,o,a&&a[i],r))}}return t}function pn(i,t,e){return gt(i)?i(t,e):i}var Wa=(i,t)=>i===!0?t:typeof i=="string"?mt(t,i):void 0;function Na(i,t,e,s,n){for(let o of t){let a=Wa(e,o);if(a){i.add(a);let r=pn(a._fallback,e,n);if(typeof r<"u"&&r!==e&&r!==s)return r}else if(a===!1&&typeof s<"u"&&e!==s)return null}return!1}function Vi(i,t,e,s){let n=t._rootScopes,o=pn(t._fallback,e,s),a=[...i,...n],r=new Set;r.add(s);let l=$s(r,a,e,o||e,s);return l===null||typeof o<"u"&&o!==e&&(l=$s(r,a,o,l,s),l===null)?!1:$e(Array.from(r),[""],n,o,()=>Ha(t,e,s))}function $s(i,t,e,s,n){for(;e;)e=Na(i,t,e,s,n);return e}function Ha(i,t,e){let s=i._getTarget();t in s||(s[t]={});let n=s[t];return F(n)&&O(e)?e:n||{}}function ja(i,t,e,s){let n;for(let o of t)if(n=mn(Ia(o,i),e),typeof n<"u")return Bi(i,n)?Vi(e,s,i,n):n}function mn(i,t){for(let e of t){if(!e)continue;let s=e[i];if(typeof s<"u")return s}}function Ys(i){let t=i._keys;return t||(t=i._keys=$a(i._scopes)),t}function $a(i){let t=new Set;for(let e of i)for(let s of Object.keys(e).filter(n=>!n.startsWith("_")))t.add(s);return Array.from(t)}function Wi(i,t,e,s){let{iScale:n}=i,{key:o="r"}=this._parsing,a=new Array(s),r,l,c,h;for(r=0,l=s;rti==="x"?"y":"x";function Ua(i,t,e,s){let n=i.skip?t:i,o=t,a=e.skip?t:e,r=Fe(o,n),l=Fe(a,o),c=r/(r+l),h=l/(r+l);c=isNaN(c)?0:c,h=isNaN(h)?0:h;let d=s*c,u=s*h;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function Xa(i,t,e){let s=i.length,n,o,a,r,l,c=$t(i,0);for(let h=0;h!c.skip)),t.cubicInterpolationMode==="monotone")qa(i,n);else{let c=s?i[i.length-1]:i[0];for(o=0,a=i.length;oi.ownerDocument.defaultView.getComputedStyle(i,null);function Za(i,t){return Xe(i).getPropertyValue(t)}var Ja=["top","right","bottom","left"];function Lt(i,t,e){let s={};e=e?"-"+e:"";for(let n=0;n<4;n++){let o=Ja[n];s[o]=parseFloat(i[t+"-"+o+e])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}var Qa=(i,t,e)=>(i>0||t>0)&&(!e||!e.shadowRoot);function tr(i,t){let e=i.touches,s=e&&e.length?e[0]:i,{offsetX:n,offsetY:o}=s,a=!1,r,l;if(Qa(n,o,i.target))r=n,l=o;else{let c=t.getBoundingClientRect();r=s.clientX-c.left,l=s.clientY-c.top,a=!0}return{x:r,y:l,box:a}}function Dt(i,t){if("native"in i)return i;let{canvas:e,currentDevicePixelRatio:s}=t,n=Xe(e),o=n.boxSizing==="border-box",a=Lt(n,"padding"),r=Lt(n,"border","width"),{x:l,y:c,box:h}=tr(i,e),d=a.left+(h&&r.left),u=a.top+(h&&r.top),{width:f,height:g}=t;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*e.width/s),y:Math.round((c-u)/g*e.height/s)}}function er(i,t,e){let s,n;if(t===void 0||e===void 0){let o=Ue(i);if(!o)t=i.clientWidth,e=i.clientHeight;else{let a=o.getBoundingClientRect(),r=Xe(o),l=Lt(r,"border","width"),c=Lt(r,"padding");t=a.width-c.width-l.width,e=a.height-c.height-l.height,s=Ie(r.maxWidth,o,"clientWidth"),n=Ie(r.maxHeight,o,"clientHeight")}}return{width:t,height:e,maxWidth:s||Ee,maxHeight:n||Ee}}var Le=i=>Math.round(i*10)/10;function _n(i,t,e,s){let n=Xe(i),o=Lt(n,"margin"),a=Ie(n.maxWidth,i,"clientWidth")||Ee,r=Ie(n.maxHeight,i,"clientHeight")||Ee,l=er(i,t,e),{width:c,height:h}=l;if(n.boxSizing==="content-box"){let u=Lt(n,"border","width"),f=Lt(n,"padding");c-=f.width+u.width,h-=f.height+u.height}return c=Math.max(0,c-o.width),h=Math.max(0,s?c/s:h-o.height),c=Le(Math.min(c,a,l.maxWidth)),h=Le(Math.min(h,r,l.maxHeight)),c&&!h&&(h=Le(c/2)),(t!==void 0||e!==void 0)&&s&&l.height&&h>l.height&&(h=l.height,c=Le(Math.floor(h*s))),{width:c,height:h}}function Ni(i,t,e){let s=t||1,n=Math.floor(i.height*s),o=Math.floor(i.width*s);i.height=Math.floor(i.height),i.width=Math.floor(i.width);let a=i.canvas;return a.style&&(e||!a.style.height&&!a.style.width)&&(a.style.height=`${i.height}px`,a.style.width=`${i.width}px`),i.currentDevicePixelRatio!==s||a.height!==n||a.width!==o?(i.currentDevicePixelRatio=s,a.height=n,a.width=o,i.ctx.setTransform(s,0,0,s,0,0),!0):!1}var yn=function(){let i=!1;try{let t={get passive(){return i=!0,!1}};Ye()&&(window.addEventListener("test",null,t),window.removeEventListener("test",null,t))}catch{}return i}();function Hi(i,t){let e=Za(i,t),s=e&&e.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function Mt(i,t,e,s){return{x:i.x+e*(t.x-i.x),y:i.y+e*(t.y-i.y)}}function vn(i,t,e,s){return{x:i.x+e*(t.x-i.x),y:s==="middle"?e<.5?i.y:t.y:s==="after"?e<1?i.y:t.y:e>0?t.y:i.y}}function Mn(i,t,e,s){let n={x:i.cp2x,y:i.cp2y},o={x:t.cp1x,y:t.cp1y},a=Mt(i,n,e),r=Mt(n,o,e),l=Mt(o,t,e),c=Mt(a,r,e),h=Mt(r,l,e);return Mt(c,h,e)}var ir=function(i,t){return{x(e){return i+i+t-e},setWidth(e){t=e},textAlign(e){return e==="center"?e:e==="right"?"left":"right"},xPlus(e,s){return e-s},leftForLtr(e,s){return e-s}}},sr=function(){return{x(i){return i},setWidth(i){},textAlign(i){return i},xPlus(i,t){return i+t},leftForLtr(i,t){return i}}};function Ft(i,t,e){return i?ir(t,e):sr()}function ji(i,t){let e,s;(t==="ltr"||t==="rtl")&&(e=i.canvas.style,s=[e.getPropertyValue("direction"),e.getPropertyPriority("direction")],e.setProperty("direction",t,"important"),i.prevTextDirection=s)}function $i(i,t){t!==void 0&&(delete i.prevTextDirection,i.canvas.style.setProperty("direction",t[0],t[1]))}function kn(i){return i==="angle"?{between:Kt,compare:ya,normalize:G}:{between:ct,compare:(t,e)=>t-e,normalize:t=>t}}function Us({start:i,end:t,count:e,loop:s,style:n}){return{start:i%e,end:t%e,loop:s&&(t-i+1)%e===0,style:n}}function nr(i,t,e){let{property:s,start:n,end:o}=e,{between:a,normalize:r}=kn(s),l=t.length,{start:c,end:h,loop:d}=i,u,f;if(d){for(c+=l,h+=l,u=0,f=l;ul(n,v,b)&&r(n,v)!==0,_=()=>r(o,b)===0||l(o,v,b),M=()=>p||y(),k=()=>!p||_();for(let w=h,S=h;w<=d;++w)x=t[w%a],!x.skip&&(b=c(x[s]),b!==v&&(p=l(b,n,o),m===null&&M()&&(m=r(b,n)===0?w:S),m!==null&&k()&&(g.push(Us({start:m,end:w,loop:u,count:a,style:f})),m=null),S=w,v=b));return m!==null&&g.push(Us({start:m,end:d,loop:u,count:a,style:f})),g}function Ui(i,t){let e=[],s=i.segments;for(let n=0;nn&&i[o%t].skip;)o--;return o%=t,{start:n,end:o}}function ar(i,t,e,s){let n=i.length,o=[],a=t,r=i[t],l;for(l=t+1;l<=e;++l){let c=i[l%n];c.skip||c.stop?r.skip||(s=!1,o.push({start:t%n,end:(l-1)%n,loop:s}),t=a=c.stop?l:null):(a=l,r.skip&&(t=l)),r=c}return a!==null&&o.push({start:t%n,end:a%n,loop:s}),o}function wn(i,t){let e=i.points,s=i.options.spanGaps,n=e.length;if(!n)return[];let o=!!i._loop,{start:a,end:r}=or(e,n,o,s);if(s===!0)return Xs(i,[{start:a,end:r,loop:o}],e,t);let l=rr({chart:t,initial:e.initial,numSteps:a,currentStep:Math.min(s-e.start,a)}))}_refresh(){this._request||(this._running=!0,this._request=Ci.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(t=Date.now()){let e=0;this._charts.forEach((s,n)=>{if(!s.running||!s.items.length)return;let o=s.items,a=o.length-1,r=!1,l;for(;a>=0;--a)l=o[a],l._active?(l._total>s.duration&&(s.duration=l._total),l.tick(t),r=!0):(o[a]=o[o.length-1],o.pop());r&&(n.draw(),this._notify(n,s,t,"progress")),o.length||(s.running=!1,this._notify(n,s,t,"complete"),s.initial=!1),e+=o.length}),this._lastDate=t,e===0&&(this._running=!1)}_getAnims(t){let e=this._charts,s=e.get(t);return s||(s={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,s)),s}listen(t,e,s){this._getAnims(t).listeners[e].push(s)}add(t,e){!e||!e.length||this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){let e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce((s,n)=>Math.max(s,n._duration),0),this._refresh())}running(t){if(!this._running)return!1;let e=this._charts.get(t);return!(!e||!e.running||!e.items.length)}stop(t){let e=this._charts.get(t);if(!e||!e.items.length)return;let s=e.items,n=s.length-1;for(;n>=0;--n)s[n].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}},xt=new is,Sn="transparent",cr={boolean(i,t,e){return e>.5?t:i},color(i,t,e){let s=Ri(i||Sn),n=s.valid&&Ri(t||Sn);return n&&n.valid?n.mix(s,e).hexString():t},number(i,t,e){return i+(t-i)*e}},ss=class{constructor(t,e,s,n){let o=e[s];n=Zt([t.to,n,o,t.from]);let a=Zt([t.from,o,n]);this._active=!0,this._fn=t.fn||cr[t.type||typeof a],this._easing=Ht[t.easing]||Ht.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=s,this._from=a,this._to=n,this._promises=void 0}active(){return this._active}update(t,e,s){if(this._active){this._notify(!1);let n=this._target[this._prop],o=s-this._start,a=this._duration-o;this._start=s,this._duration=Math.floor(Math.max(a,t.duration)),this._total+=o,this._loop=!!t.loop,this._to=Zt([t.to,e,n,t.from]),this._from=Zt([t.from,n,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){let e=t-this._start,s=this._duration,n=this._prop,o=this._from,a=this._loop,r=this._to,l;if(this._active=o!==r&&(a||e1?2-l:l,l=this._easing(Math.min(1,Math.max(0,l))),this._target[n]=this._fn(o,r,l)}wait(){let t=this._promises||(this._promises=[]);return new Promise((e,s)=>{t.push({res:e,rej:s})})}_notify(t){let e=t?"res":"rej",s=this._promises||[];for(let n=0;n{let o=t[n];if(!O(o))return;let a={};for(let r of e)a[r]=o[r];(F(o.properties)&&o.properties||[n]).forEach(r=>{(r===n||!s.has(r))&&s.set(r,a)})})}_animateOptions(t,e){let s=e.options,n=dr(t,s);if(!n)return[];let o=this._createAnimations(n,s);return s.$shared&&hr(t.options.$animations,s).then(()=>{t.options=s},()=>{}),o}_createAnimations(t,e){let s=this._properties,n=[],o=t.$animations||(t.$animations={}),a=Object.keys(e),r=Date.now(),l;for(l=a.length-1;l>=0;--l){let c=a[l];if(c.charAt(0)==="$")continue;if(c==="options"){n.push(...this._animateOptions(t,e));continue}let h=e[c],d=o[c],u=s.get(c);if(d)if(u&&d.active()){d.update(u,h,r);continue}else d.cancel();if(!u||!u.duration){t[c]=h;continue}o[c]=d=new ss(u,t,c,h),n.push(d)}return n}update(t,e){if(this._properties.size===0){Object.assign(t,e);return}let s=this._createAnimations(t,e);if(s.length)return xt.add(this._chart,s),!0}};function hr(i,t){let e=[],s=Object.keys(t);for(let n=0;n0||!e&&o<0)return n.index}return null}function An(i,t){let{chart:e,_cachedMeta:s}=i,n=e._stacks||(e._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,c=a.axis,h=pr(o,a,s),d=t.length,u;for(let f=0;fe[s].axis===t).shift()}function xr(i,t){return bt(i,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:"default",type:"dataset"})}function _r(i,t,e){return bt(i,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:e,index:t,mode:"default",type:"data"})}function fe(i,t){let e=i.controller.index,s=i.vScale&&i.vScale.axis;if(s){t=t||i._parsed;for(let n of t){let o=n._stacks;if(!o||o[s]===void 0||o[s][e]===void 0)return;delete o[s][e],o[s]._visualValues!==void 0&&o[s]._visualValues[e]!==void 0&&delete o[s]._visualValues[e]}}}var Ki=i=>i==="reset"||i==="none",Tn=(i,t)=>t?i:Object.assign({},i),yr=(i,t,e)=>i&&!t.hidden&&t._stacked&&{keys:Po(e,!0),values:null},ut=class{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){let t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Cn(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&fe(this._cachedMeta),this.index=t}linkScales(){let t=this.chart,e=this._cachedMeta,s=this.getDataset(),n=(d,u,f,g)=>d==="x"?u:d==="r"?g:f,o=e.xAxisID=P(s.xAxisID,Xi(t,"x")),a=e.yAxisID=P(s.yAxisID,Xi(t,"y")),r=e.rAxisID=P(s.rAxisID,Xi(t,"r")),l=e.indexAxis,c=e.iAxisID=n(l,o,a,r),h=e.vAxisID=n(l,a,o,r);e.xScale=this.getScaleForId(o),e.yScale=this.getScaleForId(a),e.rScale=this.getScaleForId(r),e.iScale=this.getScaleForId(c),e.vScale=this.getScaleForId(h)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){let e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){let t=this._cachedMeta;this._data&&Pi(this._data,this),t._stacked&&fe(t)}_dataCheck(){let t=this.getDataset(),e=t.data||(t.data=[]),s=this._data;if(O(e))this._data=gr(e);else if(s!==e){if(s){Pi(s,this);let n=this._cachedMeta;fe(n),n._parsed=[]}e&&Object.isExtensible(e)&&an(e,this),this._syncList=[],this._data=e}}addElements(){let t=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(t.dataset=new this.datasetElementType)}buildOrUpdateElements(t){let e=this._cachedMeta,s=this.getDataset(),n=!1;this._dataCheck();let o=e._stacked;e._stacked=Cn(e.vScale,e),e.stack!==s.stack&&(n=!0,fe(e),e.stack=s.stack),this._resyncElements(t),(n||o!==e._stacked)&&An(this,e._parsed)}configure(){let t=this.chart.config,e=t.datasetScopeKeys(this._type),s=t.getOptionScopes(this.getDataset(),e,!0);this.options=t.createResolver(s,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(t,e){let{_cachedMeta:s,_data:n}=this,{iScale:o,_stacked:a}=s,r=o.axis,l=t===0&&e===n.length?!0:s._sorted,c=t>0&&s._parsed[t-1],h,d,u;if(this._parsing===!1)s._parsed=n,s._sorted=!0,u=n;else{F(n[t])?u=this.parseArrayData(s,n,t,e):O(n[t])?u=this.parseObjectData(s,n,t,e):u=this.parsePrimitiveData(s,n,t,e);let f=()=>d[r]===null||c&&d[r]p||d=0;--u)if(!g()){this.updateRangeFromParsed(c,t,f,l);break}}return c}getAllParsedValues(t){let e=this._cachedMeta._parsed,s=[],n,o,a;for(n=0,o=e.length;n=0&&tthis.getContext(s,n,e),p=c.resolveNamedOptions(u,f,g,d);return p.$shared&&(p.$shared=l,o[a]=Object.freeze(Tn(p,l))),p}_resolveAnimations(t,e,s){let n=this.chart,o=this._cachedDataOpts,a=`animation-${e}`,r=o[a];if(r)return r;let l;if(n.options.animation!==!1){let h=this.chart.config,d=h.datasetAnimationScopeKeys(this._type,e),u=h.getOptionScopes(this.getDataset(),d);l=h.createResolver(u,this.getContext(t,s,e))}let c=new si(n,l&&l.animations);return l&&l._cacheable&&(o[a]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Ki(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){let s=this.resolveDataElementOptions(t,e),n=this._sharedOptions,o=this.getSharedOptions(s),a=this.includeOptions(e,o)||o!==n;return this.updateSharedOptions(o,e,s),{sharedOptions:o,includeOptions:a}}updateElement(t,e,s,n){Ki(n)?Object.assign(t,s):this._resolveAnimations(e,n).update(t,s)}updateSharedOptions(t,e,s){t&&!Ki(e)&&this._resolveAnimations(void 0,e).update(t,s)}_setStyle(t,e,s,n){t.active=n;let o=this.getStyle(e,n);this._resolveAnimations(e,s,n).update(t,{options:!n&&this.getSharedOptions(o)||o})}removeHoverStyle(t,e,s){this._setStyle(t,s,"active",!1)}setHoverStyle(t,e,s){this._setStyle(t,s,"active",!0)}_removeDatasetHoverStyle(){let t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){let t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){let e=this._data,s=this._cachedMeta.data;for(let[r,l,c]of this._syncList)this[r](l,c);this._syncList=[];let n=s.length,o=e.length,a=Math.min(o,n);a&&this.parse(0,a),o>n?this._insertElements(n,o-n,t):o{for(c.length+=e,r=c.length-1;r>=a;r--)c[r]=c[r-e]};for(l(o),r=t;rn-o))}return i._cache.$bar}function Mr(i){let t=i.iScale,e=vr(t,i.type),s=t._length,n,o,a,r,l=()=>{a===32767||a===-32768||(Ut(r)&&(s=Math.min(s,Math.abs(a-r)||s)),r=a)};for(n=0,o=e.length;n0?n[i-1]:null,r=iMath.abs(r)&&(l=r,c=a),t[e.axis]=c,t._custom={barStart:l,barEnd:c,start:n,end:o,min:a,max:r}}function Do(i,t,e,s){return F(i)?Sr(i,t,e,s):t[e.axis]=e.parse(i,s),t}function Ln(i,t,e,s){let n=i.iScale,o=i.vScale,a=n.getLabels(),r=n===o,l=[],c,h,d,u;for(c=e,h=e+s;c=e?1:-1)}function Dr(i){let t,e,s,n,o;return i.horizontal?(t=i.base>i.x,e="left",s="right"):(t=i.basel.controller.options.grouped),o=s.options.stacked,a=[],r=l=>{let c=l.controller.getParsed(e),h=c&&c[l.vScale.axis];if(T(h)||isNaN(h))return!0};for(let l of n)if(!(e!==void 0&&r(l))&&((o===!1||a.indexOf(l.stack)===-1||o===void 0&&l.stack===void 0)&&a.push(l.stack),l.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,s){let n=this._getStacks(t,s),o=e!==void 0?n.indexOf(e):-1;return o===-1?n.length-1:o}_getRuler(){let t=this.options,e=this._cachedMeta,s=e.iScale,n=[],o,a;for(o=0,a=e.data.length;o=0;--s)e=Math.max(e,t[s].size(this.resolveDataElementOptions(s))/2);return e>0&&e}getLabelAndValue(t){let e=this._cachedMeta,s=this.chart.data.labels||[],{xScale:n,yScale:o}=e,a=this.getParsed(t),r=n.getLabelForValue(a.x),l=o.getLabelForValue(a.y),c=a._custom;return{label:s[t]||"",value:"("+r+", "+l+(c?", "+c:"")+")"}}update(t){let e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,s,n){let o=n==="reset",{iScale:a,vScale:r}=this._cachedMeta,{sharedOptions:l,includeOptions:c}=this._getSharedOptions(e,n),h=a.axis,d=r.axis;for(let u=e;uKt(v,r,l,!0)?1:Math.max(y,y*e,_,_*e),g=(v,y,_)=>Kt(v,r,l,!0)?-1:Math.min(y,y*e,_,_*e),p=f(0,c,d),m=f(N,h,u),b=g(I,c,d),x=g(I+N,h,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}var ye=class extends ut{static id="doughnut";static defaults={datasetElementType:!1,dataElementType:"arc",animation:{animateRotate:!0,animateScale:!1},animations:{numbers:{type:"number",properties:["circumference","endAngle","innerRadius","outerRadius","startAngle","x","y","offset","borderWidth","spacing"]}},cutout:"50%",rotation:0,circumference:360,radius:"100%",spacing:0,indexAxis:"r"};static descriptors={_scriptable:t=>t!=="spacing",_indexable:t=>t!=="spacing"&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){let e=t.data;if(e.labels.length&&e.datasets.length){let{labels:{pointStyle:s,color:n}}=t.legend.options;return e.labels.map((o,a)=>{let l=t.getDatasetMeta(0).controller.getStyle(a);return{text:o,fillStyle:l.backgroundColor,strokeStyle:l.borderColor,fontColor:n,lineWidth:l.borderWidth,pointStyle:s,hidden:!t.getDataVisibility(a),index:a}})}return[]}},onClick(t,e,s){s.chart.toggleDataVisibility(e.index),s.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){let s=this.getDataset().data,n=this._cachedMeta;if(this._parsing===!1)n._parsed=s;else{let o=l=>+s[l];if(O(s[t])){let{key:l="value"}=this._parsing;o=c=>+mt(s[c],l)}let a,r;for(a=t,r=t+e;a0&&!isNaN(t)?z*(Math.abs(t)/e):0}getLabelAndValue(t){let e=this._cachedMeta,s=this.chart,n=s.data.labels||[],o=qt(e._parsed[t],s.options.locale);return{label:n[t]||"",value:o}}getMaxBorderWidth(t){let e=0,s=this.chart,n,o,a,r,l;if(!t){for(n=0,o=s.data.datasets.length;n0&&this.getParsed(e-1);for(let _=0;_=x){k.skip=!0;continue}let w=this.getParsed(_),S=T(w[f]),D=k[u]=a.getPixelForValue(w[u],_),C=k[f]=o||S?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,w,l):w[f],_);k.skip=isNaN(D)||isNaN(C)||S,k.stop=_>0&&Math.abs(w[u]-y[u])>m,p&&(k.parsed=w,k.raw=c.data[_]),d&&(k.options=h||this.resolveDataElementOptions(_,M.active?"active":n)),b||this.updateElement(M,_,k,n),y=w}}getMaxOverflow(){let t=this._cachedMeta,e=t.dataset,s=e.options&&e.options.borderWidth||0,n=t.data||[];if(!n.length)return s;let o=n[0].size(this.resolveDataElementOptions(0)),a=n[n.length-1].size(this.resolveDataElementOptions(n.length-1));return Math.max(s,o,a)/2}draw(){let t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},ni=class extends ut{static id="polarArea";static defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){let e=t.data;if(e.labels.length&&e.datasets.length){let{labels:{pointStyle:s,color:n}}=t.legend.options;return e.labels.map((o,a)=>{let l=t.getDatasetMeta(0).controller.getStyle(a);return{text:o,fillStyle:l.backgroundColor,strokeStyle:l.borderColor,fontColor:n,lineWidth:l.borderWidth,pointStyle:s,hidden:!t.getDataVisibility(a),index:a}})}return[]}},onClick(t,e,s){s.chart.toggleDataVisibility(e.index),s.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){let e=this._cachedMeta,s=this.chart,n=s.data.labels||[],o=qt(e._parsed[t].r,s.options.locale);return{label:n[t]||"",value:o}}parseObjectData(t,e,s,n){return Wi.bind(this)(t,e,s,n)}update(t){let e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){let t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach((s,n)=>{let o=this.getParsed(n).r;!isNaN(o)&&this.chart.getDataVisibility(n)&&(oe.max&&(e.max=o))}),e}_updateRadius(){let t=this.chart,e=t.chartArea,s=t.options,n=Math.min(e.right-e.left,e.bottom-e.top),o=Math.max(n/2,0),a=Math.max(s.cutoutPercentage?o/100*s.cutoutPercentage:1,0),r=(o-a)/t.getVisibleDatasetCount();this.outerRadius=o-r*this.index,this.innerRadius=this.outerRadius-r}updateElements(t,e,s,n){let o=n==="reset",a=this.chart,l=a.options.animation,c=this._cachedMeta.rScale,h=c.xCenter,d=c.yCenter,u=c.getIndexAngle(0)-.5*I,f=u,g,p=360/this.countVisibleElements();for(g=0;g{!isNaN(this.getParsed(n).r)&&this.chart.getDataVisibility(n)&&e++}),e}_computeAngle(t,e,s){return this.chart.getDataVisibility(t)?it(this.resolveDataElementOptions(t,e).angle||s):0}},rs=class extends ye{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},ls=class extends ut{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){let e=this._cachedMeta.vScale,s=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(s[e.axis])}}parseObjectData(t,e,s,n){return Wi.bind(this)(t,e,s,n)}update(t){let e=this._cachedMeta,s=e.dataset,n=e.data||[],o=e.iScale.getLabels();if(s.points=n,t!=="resize"){let a=this.resolveDatasetElementOptions(t);this.options.showLine||(a.borderWidth=0);let r={_loop:!0,_fullLoop:o.length===n.length,options:a};this.updateElement(s,void 0,r,t)}this.updateElements(n,0,n.length,t)}updateElements(t,e,s,n){let o=this._cachedMeta.rScale,a=n==="reset";for(let r=e;r0&&this.getParsed(e-1);for(let y=e;y0&&Math.abs(M[f]-v[f])>b,m&&(k.parsed=M,k.raw=c.data[y]),u&&(k.options=d||this.resolveDataElementOptions(y,_.active?"active":n)),x||this.updateElement(_,y,k,n),v=M}this.updateSharedOptions(d,n,h)}getMaxOverflow(){let t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let r=0;for(let l=e.length-1;l>=0;--l)r=Math.max(r,e[l].size(this.resolveDataElementOptions(l))/2);return r>0&&r}let s=t.dataset,n=s.options&&s.options.borderWidth||0;if(!e.length)return n;let o=e[0].size(this.resolveDataElementOptions(0)),a=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(n,o,a)/2}},Lr=Object.freeze({__proto__:null,BarController:ns,BubbleController:os,DoughnutController:ye,LineController:as,PieController:rs,PolarAreaController:ni,RadarController:ls,ScatterController:cs});function It(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}var hs=class i{static override(t){Object.assign(i.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return It()}parse(){return It()}format(){return It()}add(){return It()}diff(){return It()}startOf(){return It()}endOf(){return It()}},Rr={_date:hs};function Er(i,t,e,s){let{controller:n,data:o,_sorted:a}=i,r=n._cachedMeta.iScale;if(r&&t===r.axis&&t!=="r"&&a&&o.length){let l=r._reversePixels?sn:at;if(s){if(n._sharedOptions){let c=o[0],h=typeof c.getRange=="function"&&c.getRange(t);if(h){let d=l(o,t,e-h),u=l(o,t,e+h);return{lo:d.lo,hi:u.hi}}}}else return l(o,t,e)}return{lo:0,hi:o.length-1}}function Se(i,t,e,s,n){let o=i.getSortedVisibleDatasetMetas(),a=e[t];for(let r=0,l=o.length;r{l[a](t[e],n)&&(o.push({element:l,datasetIndex:c,index:h}),r=r||l.inRange(t.x,t.y,n))}),s&&!r?[]:o}var Br={evaluateInteractionItems:Se,modes:{index(i,t,e,s){let n=Dt(t,i),o=e.axis||"x",a=e.includeInvisible||!1,r=e.intersect?Gi(i,n,o,s,a):Zi(i,n,o,!1,s,a),l=[];return r.length?(i.getSortedVisibleDatasetMetas().forEach(c=>{let h=r[0].index,d=c.data[h];d&&!d.skip&&l.push({element:d,datasetIndex:c.index,index:h})}),l):[]},dataset(i,t,e,s){let n=Dt(t,i),o=e.axis||"xy",a=e.includeInvisible||!1,r=e.intersect?Gi(i,n,o,s,a):Zi(i,n,o,!1,s,a);if(r.length>0){let l=r[0].datasetIndex,c=i.getDatasetMeta(l).data;r=[];for(let h=0;he.pos===t)}function In(i,t){return i.filter(e=>Co.indexOf(e.pos)===-1&&e.box.axis===t)}function pe(i,t){return i.sort((e,s)=>{let n=t?s:e,o=t?e:s;return n.weight===o.weight?n.index-o.index:n.weight-o.weight})}function Vr(i){let t=[],e,s,n,o,a,r;for(e=0,s=(i||[]).length;ec.box.fullSize),!0),s=pe(ge(t,"left"),!0),n=pe(ge(t,"right")),o=pe(ge(t,"top"),!0),a=pe(ge(t,"bottom")),r=In(t,"x"),l=In(t,"y");return{fullSize:e,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:ge(t,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}function zn(i,t,e,s){return Math.max(i[e],t[e])+Math.max(i[s],t[s])}function Oo(i,t){i.top=Math.max(i.top,t.top),i.left=Math.max(i.left,t.left),i.bottom=Math.max(i.bottom,t.bottom),i.right=Math.max(i.right,t.right)}function jr(i,t,e,s){let{pos:n,box:o}=e,a=i.maxPadding;if(!O(n)){e.size&&(i[n]-=e.size);let d=s[e.stack]||{size:0,count:1};d.size=Math.max(d.size,e.horizontal?o.height:o.width),e.size=d.size/d.count,i[n]+=e.size}o.getPadding&&Oo(a,o.getPadding());let r=Math.max(0,t.outerWidth-zn(a,i,"left","right")),l=Math.max(0,t.outerHeight-zn(a,i,"top","bottom")),c=r!==i.w,h=l!==i.h;return i.w=r,i.h=l,e.horizontal?{same:c,other:h}:{same:h,other:c}}function $r(i){let t=i.maxPadding;function e(s){let n=Math.max(t[s]-i[s],0);return i[s]+=n,n}i.y+=e("top"),i.x+=e("left"),e("right"),e("bottom")}function Yr(i,t){let e=t.maxPadding;function s(n){let o={left:0,top:0,right:0,bottom:0};return n.forEach(a=>{o[a]=Math.max(t[a],e[a])}),o}return s(i?["left","right"]:["top","bottom"])}function xe(i,t,e,s){let n=[],o,a,r,l,c,h;for(o=0,a=i.length,c=0;o{typeof p.beforeLayout=="function"&&p.beforeLayout()});let h=l.reduce((p,m)=>m.box.options&&m.box.options.display===!1?p:p+1,0)||1,d=Object.freeze({outerWidth:t,outerHeight:e,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/h,hBoxMaxHeight:a/2}),u=Object.assign({},n);Oo(u,X(s));let f=Object.assign({maxPadding:u,w:o,h:a,x:n.left,y:n.top},n),g=Nr(l.concat(c),d);xe(r.fullSize,f,d,g),xe(l,f,d,g),xe(c,f,d,g)&&xe(l,f,d,g),$r(f),Bn(r.leftAndTop,f,d,g),f.x+=f.w,f.y+=f.h,Bn(r.rightAndBottom,f,d,g),i.chartArea={left:f.left,top:f.top,right:f.left+f.w,bottom:f.top+f.h,height:f.h,width:f.w},L(r.chartArea,p=>{let m=p.box;Object.assign(m,i.chartArea),m.update(f.w,f.h,{left:0,top:0,right:0,bottom:0})})}},oi=class{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,s){}removeEventListener(t,e,s){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,s,n){return e=Math.max(0,e||t.width),s=s||t.height,{width:e,height:Math.max(0,n?Math.floor(e/n):s)}}isAttached(t){return!0}updateConfig(t){}},ds=class extends oi{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}},ei="$chartjs",Ur={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},Vn=i=>i===null||i==="";function Xr(i,t){let e=i.style,s=i.getAttribute("height"),n=i.getAttribute("width");if(i[ei]={initial:{height:s,width:n,style:{display:e.display,height:e.height,width:e.width}}},e.display=e.display||"block",e.boxSizing=e.boxSizing||"border-box",Vn(n)){let o=Hi(i,"width");o!==void 0&&(i.width=o)}if(Vn(s))if(i.style.height==="")i.height=i.width/(t||2);else{let o=Hi(i,"height");o!==void 0&&(i.height=o)}return i}var Ao=yn?{passive:!0}:!1;function Kr(i,t,e){i&&i.addEventListener(t,e,Ao)}function qr(i,t,e){i&&i.canvas&&i.canvas.removeEventListener(t,e,Ao)}function Gr(i,t){let e=Ur[i.type]||i.type,{x:s,y:n}=Dt(i,t);return{type:e,chart:t,native:i,x:s!==void 0?s:null,y:n!==void 0?n:null}}function ai(i,t){for(let e of i)if(e===t||e.contains(t))return!0}function Zr(i,t,e){let s=i.canvas,n=new MutationObserver(o=>{let a=!1;for(let r of o)a=a||ai(r.addedNodes,s),a=a&&!ai(r.removedNodes,s);a&&e()});return n.observe(document,{childList:!0,subtree:!0}),n}function Jr(i,t,e){let s=i.canvas,n=new MutationObserver(o=>{let a=!1;for(let r of o)a=a||ai(r.removedNodes,s),a=a&&!ai(r.addedNodes,s);a&&e()});return n.observe(document,{childList:!0,subtree:!0}),n}var ve=new Map,Wn=0;function To(){let i=window.devicePixelRatio;i!==Wn&&(Wn=i,ve.forEach((t,e)=>{e.currentDevicePixelRatio!==i&&t()}))}function Qr(i,t){ve.size||window.addEventListener("resize",To),ve.set(i,t)}function tl(i){ve.delete(i),ve.size||window.removeEventListener("resize",To)}function el(i,t,e){let s=i.canvas,n=s&&Ue(s);if(!n)return;let o=Oi((r,l)=>{let c=n.clientWidth;e(r,l),c{let l=r[0],c=l.contentRect.width,h=l.contentRect.height;c===0&&h===0||o(c,h)});return a.observe(n),Qr(i,o),a}function Ji(i,t,e){e&&e.disconnect(),t==="resize"&&tl(i)}function il(i,t,e){let s=i.canvas,n=Oi(o=>{i.ctx!==null&&e(Gr(o,i))},i);return Kr(s,t,n),n}var us=class extends oi{acquireContext(t,e){let s=t&&t.getContext&&t.getContext("2d");return s&&s.canvas===t?(Xr(t,e),s):null}releaseContext(t){let e=t.canvas;if(!e[ei])return!1;let s=e[ei].initial;["height","width"].forEach(o=>{let a=s[o];T(a)?e.removeAttribute(o):e.setAttribute(o,a)});let n=s.style||{};return Object.keys(n).forEach(o=>{e.style[o]=n[o]}),e.width=e.width,delete e[ei],!0}addEventListener(t,e,s){this.removeEventListener(t,e);let n=t.$proxies||(t.$proxies={}),a={attach:Zr,detach:Jr,resize:el}[e]||il;n[e]=a(t,e,s)}removeEventListener(t,e){let s=t.$proxies||(t.$proxies={}),n=s[e];if(!n)return;({attach:Ji,detach:Ji,resize:Ji}[e]||qr)(t,e,n),s[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,s,n){return _n(t,e,s,n)}isAttached(t){let e=Ue(t);return!!(e&&e.isConnected)}};function sl(i){return!Ye()||typeof OffscreenCanvas<"u"&&i instanceof OffscreenCanvas?ds:us}var ot=class{static defaults={};static defaultRoutes=void 0;x;y;active=!1;options;$animations;tooltipPosition(t){let{x:e,y:s}=this.getProps(["x","y"],t);return{x:e,y:s}}hasValue(){return Et(this.x)&&Et(this.y)}getProps(t,e){let s=this.$animations;if(!e||!s)return this;let n={};return t.forEach(o=>{n[o]=s[o]&&s[o].active()?s[o]._to:this[o]}),n}};function nl(i,t){let e=i.options.ticks,s=ol(i),n=Math.min(e.maxTicksLimit||s,s),o=e.major.enabled?rl(t):[],a=o.length,r=o[0],l=o[a-1],c=[];if(a>n)return ll(t,c,o,a/n),c;let h=al(o,t,n);if(a>0){let d,u,f=a>1?Math.round((l-r)/(a-1)):null;for(qe(t,c,h,T(f)?0:r-f,r),d=0,u=a-1;dn)return l}return Math.max(n,1)}function rl(i){let t=[],e,s;for(e=0,s=i.length;ei==="left"?"right":i==="right"?"left":i,Nn=(i,t,e)=>t==="top"||t==="left"?i[t]+e:i[t]-e,Hn=(i,t)=>Math.min(t||i,i);function jn(i,t){let e=[],s=i.length/t,n=i.length,o=0;for(;oa+r)))return l}function ul(i,t){L(i,e=>{let s=e.gc,n=s.length/2,o;if(n>t){for(o=0;os?s:e,s=n&&e>s?e:s,{min:Z(e,Z(s,e)),max:Z(s,Z(e,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){let t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){E(this.options.beforeUpdate,[this])}update(t,e,s){let{beginAtZero:n,grace:o,ticks:a}=this.options,r=a.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=s=Object.assign({left:0,right:0,top:0,bottom:0},s),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+s.left+s.right:this.height+s.top+s.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=fn(this,o,n),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();let l=r=o||s<=1||!this.isHorizontal()){this.labelRotation=n;return}let h=this._getLabelSizes(),d=h.widest.width,u=h.highest.height,f=$(this.chart.width-d,0,this.maxWidth);r=t.offset?this.maxWidth/s:f/(s-1),d+6>r&&(r=f/(s-(t.offset?.5:1)),l=this.maxHeight-me(t.grid)-e.padding-$n(t.title,this.chart.options.font),c=Math.sqrt(d*d+u*u),a=Be(Math.min(Math.asin($((h.highest.height+6)/r,-1,1)),Math.asin($(l/c,-1,1))-Math.asin($(u/c,-1,1)))),a=Math.max(n,Math.min(o,a))),this.labelRotation=a}afterCalculateLabelRotation(){E(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){E(this.options.beforeFit,[this])}fit(){let t={width:0,height:0},{chart:e,options:{ticks:s,title:n,grid:o}}=this,a=this._isVisible(),r=this.isHorizontal();if(a){let l=$n(n,e.options.font);if(r?(t.width=this.maxWidth,t.height=me(o)+l):(t.height=this.maxHeight,t.width=me(o)+l),s.display&&this.ticks.length){let{first:c,last:h,widest:d,highest:u}=this._getLabelSizes(),f=s.padding*2,g=it(this.labelRotation),p=Math.cos(g),m=Math.sin(g);if(r){let b=s.mirror?0:m*d.width+p*u.height;t.height=Math.min(this.maxHeight,t.height+b+f)}else{let b=s.mirror?0:p*d.width+m*u.height;t.width=Math.min(this.maxWidth,t.width+b+f)}this._calculatePadding(c,h,m,p)}}this._handleMargins(),r?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,s,n){let{ticks:{align:o,padding:a},position:r}=this.options,l=this.labelRotation!==0,c=r!=="top"&&this.axis==="x";if(this.isHorizontal()){let h=this.getPixelForTick(0)-this.left,d=this.right-this.getPixelForTick(this.ticks.length-1),u=0,f=0;l?c?(u=n*t.width,f=s*e.height):(u=s*t.height,f=n*e.width):o==="start"?f=e.width:o==="end"?u=t.width:o!=="inner"&&(u=t.width/2,f=e.width/2),this.paddingLeft=Math.max((u-h+a)*this.width/(this.width-h),0),this.paddingRight=Math.max((f-d+a)*this.width/(this.width-d),0)}else{let h=e.height/2,d=t.height/2;o==="start"?(h=0,d=t.height):o==="end"&&(h=e.height,d=0),this.paddingTop=h+a,this.paddingBottom=d+a}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){E(this.options.afterFit,[this])}isHorizontal(){let{axis:t,position:e}=this.options;return e==="top"||e==="bottom"||t==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){this.beforeTickToLabelConversion(),this.generateTickLabels(t);let e,s;for(e=0,s=t.length;e({width:a[S]||0,height:r[S]||0});return{first:w(0),last:w(e-1),widest:w(M),highest:w(k),widths:a,heights:r}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){let e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);let e=this._startPixel+t*this._length;return en(this._alignToPixels?wt(this.chart,e,0):e)}getDecimalForPixel(t){let e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){let{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){let e=this.ticks||[];if(t>=0&&tr*n?r/s:l/n:l*n0}_computeGridLineItems(t){let e=this.axis,s=this.chart,n=this.options,{grid:o,position:a,border:r}=n,l=o.offset,c=this.isHorizontal(),d=this.ticks.length+(l?1:0),u=me(o),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(B){return wt(s,B,p)},x,v,y,_,M,k,w,S,D,C,A,Y;if(a==="top")x=b(this.bottom),k=this.bottom-u,S=x-m,C=b(t.top)+m,Y=t.bottom;else if(a==="bottom")x=b(this.top),C=t.top,Y=b(t.bottom)-m,k=x+m,S=this.top+u;else if(a==="left")x=b(this.right),M=this.right-u,w=x-m,D=b(t.left)+m,A=t.right;else if(a==="right")x=b(this.left),D=t.left,A=b(t.right)-m,M=x+m,w=this.left+u;else if(e==="x"){if(a==="center")x=b((t.top+t.bottom)/2+.5);else if(O(a)){let B=Object.keys(a)[0],H=a[B];x=b(this.chart.scales[B].getPixelForValue(H))}C=t.top,Y=t.bottom,k=x+m,S=k+u}else if(e==="y"){if(a==="center")x=b((t.left+t.right)/2);else if(O(a)){let B=Object.keys(a)[0],H=a[B];x=b(this.chart.scales[B].getPixelForValue(H))}M=x-m,w=M-u,D=t.left,A=t.right}let tt=P(n.ticks.maxTicksLimit,d),R=Math.max(1,Math.ceil(d/tt));for(v=0;v0&&(At-=Ot/2);break}Pe={left:At,top:se,width:Ot+Wt.width,height:ie+Wt.height,color:R.backdropColor}}m.push({label:y,font:S,textOffset:A,options:{rotation:p,color:H,strokeColor:st,strokeWidth:K,textAlign:Vt,textBaseline:Y,translation:[_,M],backdrop:Pe}})}return m}_getXAxisLabelAlignment(){let{position:t,ticks:e}=this.options;if(-it(this.labelRotation))return t==="top"?"left":"right";let n="center";return e.align==="start"?n="left":e.align==="end"?n="right":e.align==="inner"&&(n="inner"),n}_getYAxisLabelAlignment(t){let{position:e,ticks:{crossAlign:s,mirror:n,padding:o}}=this.options,a=this._getLabelSizes(),r=t+o,l=a.widest.width,c,h;return e==="left"?n?(h=this.right+o,s==="near"?c="left":s==="center"?(c="center",h+=l/2):(c="right",h+=l)):(h=this.right-r,s==="near"?c="right":s==="center"?(c="center",h-=l/2):(c="left",h=this.left)):e==="right"?n?(h=this.left+o,s==="near"?c="right":s==="center"?(c="center",h-=l/2):(c="left",h-=l)):(h=this.left+r,s==="near"?c="left":s==="center"?(c="center",h+=l/2):(c="right",h=this.right)):c="right",{textAlign:c,x:h}}_computeLabelArea(){if(this.options.ticks.mirror)return;let t=this.chart,e=this.options.position;if(e==="left"||e==="right")return{top:0,left:this.left,bottom:t.height,right:this.right};if(e==="top"||e==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:t.width}}drawBackground(){let{ctx:t,options:{backgroundColor:e},left:s,top:n,width:o,height:a}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(s,n,o,a),t.restore())}getLineWidthForValue(t){let e=this.options.grid;if(!this._isVisible()||!e.display)return 0;let n=this.ticks.findIndex(o=>o.value===t);return n>=0?e.setContext(this.getContext(n)).lineWidth:0}drawGrid(t){let e=this.options.grid,s=this.ctx,n=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t)),o,a,r=(l,c,h)=>{!h.width||!h.color||(s.save(),s.lineWidth=h.width,s.strokeStyle=h.color,s.setLineDash(h.borderDash||[]),s.lineDashOffset=h.borderDashOffset,s.beginPath(),s.moveTo(l.x,l.y),s.lineTo(c.x,c.y),s.stroke(),s.restore())};if(e.display)for(o=0,a=n.length;o{this.draw(o)}}]:[{z:s,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:n,draw:()=>{this.drawBorder()}},{z:e,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(t){let e=this.chart.getSortedVisibleDatasetMetas(),s=this.axis+"AxisID",n=[],o,a;for(o=0,a=e.length;o{let s=e.split("."),n=s.pop(),o=[i].concat(s).join("."),a=t[e].split("."),r=a.pop(),l=a.join(".");W.route(o,n,l,r)})}function _l(i){return"id"in i&&"defaults"in i}var fs=class{constructor(){this.controllers=new Qt(ut,"datasets",!0),this.elements=new Qt(ot,"elements"),this.plugins=new Qt(Object,"plugins"),this.scales=new Qt(Bt,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,s){[...e].forEach(n=>{let o=s||this._getRegistryForType(n);s||o.isForType(n)||o===this.plugins&&n.id?this._exec(t,o,n):L(n,a=>{let r=s||this._getRegistryForType(a);this._exec(t,r,a)})})}_exec(t,e,s){let n=ze(t);E(s["before"+n],[],s),e[t](s),E(s["after"+n],[],s)}_getRegistryForType(t){for(let e=0;eo.filter(r=>!a.some(l=>r.plugin.id===l.plugin.id));this._notify(n(e,s),t,"stop"),this._notify(n(s,e),t,"start")}};function yl(i){let t={},e=[],s=Object.keys(dt.plugins.items);for(let o=0;o1&&Yn(i[0].toLowerCase());if(s)return s}throw new Error(`Cannot determine type of '${i}' axis. Please provide 'axis' or 'position' option.`)}function Un(i,t,e){if(e[t+"AxisID"]===i)return{axis:t}}function Dl(i,t){if(t.data&&t.data.datasets){let e=t.data.datasets.filter(s=>s.xAxisID===i||s.yAxisID===i);if(e.length)return Un(i,"x",e[0])||Un(i,"y",e[0])}return{}}function Cl(i,t){let e=kt[i.type]||{scales:{}},s=t.scales||{},n=ps(i.type,t),o=Object.create(null);return Object.keys(s).forEach(a=>{let r=s[a];if(!O(r))return console.error(`Invalid scale configuration for scale: ${a}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${a}`);let l=ms(a,r,Dl(a,i),W.scales[r.type]),c=Sl(l,n),h=e.scales||{};o[a]=Yt(Object.create(null),[{axis:l},r,h[l],h[c]])}),i.data.datasets.forEach(a=>{let r=a.type||i.type,l=a.indexAxis||ps(r,t),h=(kt[r]||{}).scales||{};Object.keys(h).forEach(d=>{let u=wl(d,l),f=a[u+"AxisID"]||u;o[f]=o[f]||Object.create(null),Yt(o[f],[{axis:u},s[f],h[d]])})}),Object.keys(o).forEach(a=>{let r=o[a];Yt(r,[W.scales[r.type],W.scale])}),o}function Lo(i){let t=i.options||(i.options={});t.plugins=P(t.plugins,{}),t.scales=Cl(i,t)}function Ro(i){return i=i||{},i.datasets=i.datasets||[],i.labels=i.labels||[],i}function Ol(i){return i=i||{},i.data=Ro(i.data),Lo(i),i}var Xn=new Map,Eo=new Set;function Ge(i,t){let e=Xn.get(i);return e||(e=t(),Xn.set(i,e),Eo.add(e)),e}var be=(i,t,e)=>{let s=mt(t,e);s!==void 0&&i.add(s)},bs=class{constructor(t){this._config=Ol(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=Ro(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){let t=this._config;this.clearCache(),Lo(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return Ge(t,()=>[[`datasets.${t}`,""]])}datasetAnimationScopeKeys(t,e){return Ge(`${t}.transition.${e}`,()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]])}datasetElementScopeKeys(t,e){return Ge(`${t}-${e}`,()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]])}pluginScopeKeys(t){let e=t.id,s=this.type;return Ge(`${s}-plugin-${e}`,()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]])}_cachedScopes(t,e){let s=this._scopeCache,n=s.get(t);return(!n||e)&&(n=new Map,s.set(t,n)),n}getOptionScopes(t,e,s){let{options:n,type:o}=this,a=this._cachedScopes(t,s),r=a.get(e);if(r)return r;let l=new Set;e.forEach(h=>{t&&(l.add(t),h.forEach(d=>be(l,t,d))),h.forEach(d=>be(l,n,d)),h.forEach(d=>be(l,kt[o]||{},d)),h.forEach(d=>be(l,W,d)),h.forEach(d=>be(l,Ne,d))});let c=Array.from(l);return c.length===0&&c.push(Object.create(null)),Eo.has(e)&&a.set(e,c),c}chartOptionScopes(){let{options:t,type:e}=this;return[t,kt[e]||{},W.datasets[e]||{},{type:e},W,Ne]}resolveNamedOptions(t,e,s,n=[""]){let o={$shared:!0},{resolver:a,subPrefixes:r}=Kn(this._resolverCache,t,n),l=a;if(Tl(a,e)){o.$shared=!1,s=gt(s)?s():s;let c=this.createResolver(t,s,r);l=Rt(a,s,c)}for(let c of e)o[c]=l[c];return o}createResolver(t,e,s=[""],n){let{resolver:o}=Kn(this._resolverCache,t,s);return O(e)?Rt(o,e,void 0,n):o}};function Kn(i,t,e){let s=i.get(t);s||(s=new Map,i.set(t,s));let n=e.join(),o=s.get(n);return o||(o={resolver:$e(t,e),subPrefixes:e.filter(r=>!r.toLowerCase().includes("hover"))},s.set(n,o)),o}var Al=i=>O(i)&&Object.getOwnPropertyNames(i).some(t=>gt(i[t]));function Tl(i,t){let{isScriptable:e,isIndexable:s}=zi(i);for(let n of t){let o=e(n),a=s(n),r=(a||o)&&i[n];if(o&&(gt(r)||Al(r))||a&&F(r))return!0}return!1}var Ll="4.4.2",Rl=["top","bottom","left","right","chartArea"];function qn(i,t){return i==="top"||i==="bottom"||Rl.indexOf(i)===-1&&t==="x"}function Gn(i,t){return function(e,s){return e[i]===s[i]?e[t]-s[t]:e[i]-s[i]}}function Zn(i){let t=i.chart,e=t.options.animation;t.notifyPlugins("afterRender"),E(e&&e.onComplete,[i],t)}function El(i){let t=i.chart,e=t.options.animation;E(e&&e.onProgress,[i],t)}function Fo(i){return Ye()&&typeof i=="string"?i=document.getElementById(i):i&&i.length&&(i=i[0]),i&&i.canvas&&(i=i.canvas),i}var ii={},Jn=i=>{let t=Fo(i);return Object.values(ii).filter(e=>e.canvas===t).pop()};function Fl(i,t,e){let s=Object.keys(i);for(let n of s){let o=+n;if(o>=t){let a=i[n];delete i[n],(e>0||o>t)&&(i[o+e]=a)}}}function Il(i,t,e,s){return!e||i.type==="mouseout"?null:s?t:i}function Ze(i,t,e){return i.options.clip?i[e]:t[e]}function zl(i,t){let{xScale:e,yScale:s}=i;return e&&s?{left:Ze(e,t,"left"),right:Ze(e,t,"right"),top:Ze(s,t,"top"),bottom:Ze(s,t,"bottom")}:t}var xs=class{static defaults=W;static instances=ii;static overrides=kt;static registry=dt;static version=Ll;static getChart=Jn;static register(...t){dt.add(...t),Qn()}static unregister(...t){dt.remove(...t),Qn()}constructor(t,e){let s=this.config=new bs(e),n=Fo(t),o=Jn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");let a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||sl(n)),this.platform.updateConfig(s);let r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,c=l&&l.height,h=l&&l.width;if(this.id=qs(),this.ctx=r,this.canvas=l,this.width=h,this.height=c,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new gs,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=rn(d=>this.update(d),a.resizeDelay||0),this._dataChanges=[],ii[this.id]=this,!r||!l){console.error("Failed to create chart: can't acquire context from the given item");return}xt.listen(this,"complete",Zn),xt.listen(this,"progress",El),this._initialize(),this.attached&&this.update()}get aspectRatio(){let{options:{aspectRatio:t,maintainAspectRatio:e},width:s,height:n,_aspectRatio:o}=this;return T(t)?e&&o?o:n?s/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return dt}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():Ni(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Ei(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){let s=this.options,n=this.canvas,o=s.maintainAspectRatio&&this.aspectRatio,a=this.platform.getMaximumSize(n,t,e,o),r=s.devicePixelRatio||this.platform.getDevicePixelRatio(),l=this.width?"resize":"attach";this.width=a.width,this.height=a.height,this._aspectRatio=this.aspectRatio,Ni(this,r,!0)&&(this.notifyPlugins("resize",{size:a}),E(s.onResize,[this,a],this),this.attached&&this._doResize(l)&&this.render())}ensureScalesHaveIDs(){let e=this.options.scales||{};L(e,(s,n)=>{s.id=n})}buildOrUpdateScales(){let t=this.options,e=t.scales,s=this.scales,n=Object.keys(s).reduce((a,r)=>(a[r]=!1,a),{}),o=[];e&&(o=o.concat(Object.keys(e).map(a=>{let r=e[a],l=ms(a,r),c=l==="r",h=l==="x";return{options:r,dposition:c?"chartArea":h?"bottom":"left",dtype:c?"radialLinear":h?"category":"linear"}}))),L(o,a=>{let r=a.options,l=r.id,c=ms(l,r),h=P(r.type,a.dtype);(r.position===void 0||qn(r.position,c)!==qn(a.dposition))&&(r.position=a.dposition),n[l]=!0;let d=null;if(l in s&&s[l].type===h)d=s[l];else{let u=dt.getScale(h);d=new u({id:l,type:h,ctx:this.ctx,chart:this}),s[d.id]=d}d.init(r,t)}),L(n,(a,r)=>{a||delete s[r]}),L(s,a=>{q.configure(this,a,a.options),q.addBox(this,a)})}_updateMetasets(){let t=this._metasets,e=this.data.datasets.length,s=t.length;if(t.sort((n,o)=>n.index-o.index),s>e){for(let n=e;ne.length&&delete this._stacks,t.forEach((s,n)=>{e.filter(o=>o===s._dataset).length===0&&this._destroyDatasetMeta(n)})}buildOrUpdateControllers(){let t=[],e=this.data.datasets,s,n;for(this._removeUnreferencedMetasets(),s=0,n=e.length;s{this.getDatasetMeta(e).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){let e=this.config;e.update();let s=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),n=this._animationsDisabled=!s.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0})===!1)return;let o=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let a=0;for(let c=0,h=this.data.datasets.length;c{c.reset()}),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Gn("z","_idx"));let{_active:r,_lastEvent:l}=this;l?this._eventHandler(l,!0):r.length&&this._updateHoverStyles(r,r,!0),this.render()}_updateScales(){L(this.scales,t=>{q.removeBox(this,t)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let t=this.options,e=new Set(Object.keys(this._listeners)),s=new Set(t.events);(!vi(e,s)||!!this._responsiveListeners!==t.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){let{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(let{method:s,start:n,count:o}of e){let a=s==="_removeElements"?-o:o;Fl(t,n,a)}}_getUniformDataChanges(){let t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];let e=this.data.datasets.length,s=o=>new Set(t.filter(a=>a[0]===o).map((a,r)=>r+","+a.splice(1).join(","))),n=s(0);for(let o=1;oo.split(",")).map(o=>({method:o[1],start:+o[2],count:+o[3]}))}_updateLayout(t){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;q.update(this,this.width,this.height,t);let e=this.chartArea,s=e.width<=0||e.height<=0;this._layers=[],L(this.boxes,n=>{s&&n.position==="chartArea"||(n.configure&&n.configure(),this._layers.push(...n._layers()))},this),this._layers.forEach((n,o)=>{n._idx=o}),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})!==!1){for(let e=0,s=this.data.datasets.length;e=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){let e=this.ctx,s=t._clip,n=!s.disabled,o=zl(t,this.chartArea),a={meta:t,index:t.index,cancelable:!0};this.notifyPlugins("beforeDatasetDraw",a)!==!1&&(n&&de(e,{left:s.left===!1?0:o.left-s.left,right:s.right===!1?this.width:o.right+s.right,top:s.top===!1?0:o.top-s.top,bottom:s.bottom===!1?this.height:o.bottom+s.bottom}),t.controller.draw(),n&&ue(e),a.cancelable=!1,this.notifyPlugins("afterDatasetDraw",a))}isPointInArea(t){return rt(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,s,n){let o=Br.modes[e];return typeof o=="function"?o(this,t,s,n):[]}getDatasetMeta(t){let e=this.data.datasets[t],s=this._metasets,n=s.filter(o=>o&&o._dataset===e).pop();return n||(n={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},s.push(n)),n}getContext(){return this.$context||(this.$context=bt(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){let e=this.data.datasets[t];if(!e)return!1;let s=this.getDatasetMeta(t);return typeof s.hidden=="boolean"?!s.hidden:!e.hidden}setDatasetVisibility(t,e){let s=this.getDatasetMeta(t);s.hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,s){let n=s?"show":"hide",o=this.getDatasetMeta(t),a=o.controller._resolveAnimations(void 0,n);Ut(e)?(o.data[e].hidden=!s,this.update()):(this.setDatasetVisibility(t,s),a.update(o,{visible:s}),this.update(r=>r.datasetIndex===t?n:void 0))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){let e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,o,a),t[o]=a},n=(o,a,r)=>{o.offsetX=a,o.offsetY=r,this._eventHandler(o)};L(this.options.events,o=>s(o,n))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});let t=this._responsiveListeners,e=this.platform,s=(l,c)=>{e.addEventListener(this,l,c),t[l]=c},n=(l,c)=>{t[l]&&(e.removeEventListener(this,l,c),delete t[l])},o=(l,c)=>{this.canvas&&this.resize(l,c)},a,r=()=>{n("attach",r),this.attached=!0,this.resize(),s("resize",o),s("detach",a)};a=()=>{this.attached=!1,n("resize",o),this._stop(),this._resize(0,0),s("attach",r)},e.isAttached(this.canvas)?r():a()}unbindEvents(){L(this._listeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._listeners={},L(this._responsiveListeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._responsiveListeners=void 0}updateHoverStyle(t,e,s){let n=s?"set":"remove",o,a,r,l;for(e==="dataset"&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+n+"DatasetHoverStyle"]()),r=0,l=t.length;r{let r=this.getDatasetMeta(o);if(!r)throw new Error("No dataset found at index "+o);return{datasetIndex:o,element:r.data[a],index:a}});!ce(s,e)&&(this._active=s,this._lastEvent=null,this._updateHoverStyles(s,e))}notifyPlugins(t,e,s){return this._plugins.notify(this,t,e,s)}isPluginEnabled(t){return this._plugins._cache.filter(e=>e.plugin.id===t).length===1}_updateHoverStyles(t,e,s){let n=this.options.hover,o=(l,c)=>l.filter(h=>!c.some(d=>h.datasetIndex===d.datasetIndex&&h.index===d.index)),a=o(e,t),r=s?t:o(t,e);a.length&&this.updateHoverStyle(a,n.mode,!1),r.length&&n.mode&&this.updateHoverStyle(r,n.mode,!0)}_eventHandler(t,e){let s={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},n=a=>(a.options.events||this.options.events).includes(t.native.type);if(this.notifyPlugins("beforeEvent",s,n)===!1)return;let o=this._handleEvent(t,e,s.inChartArea);return s.cancelable=!1,this.notifyPlugins("afterEvent",s,n),(o||s.changed)&&this.render(),this}_handleEvent(t,e,s){let{_active:n=[],options:o}=this,a=e,r=this._getActiveElements(t,n,s,a),l=Js(t),c=Il(t,this._lastEvent,s,l);s&&(this._lastEvent=null,E(o.onHover,[t,r,this],this),l&&E(o.onClick,[t,r,this],this));let h=!ce(r,n);return(h||e)&&(this._active=r,this._updateHoverStyles(r,n,e)),this._lastEvent=c,h}_getActiveElements(t,e,s,n){if(t.type==="mouseout")return[];if(!s)return e;let o=this.options.hover;return this.getElementsAtEventForMode(t,o.mode,o,n)}};function Qn(){return L(xs.instances,i=>i._plugins.invalidate())}function Bl(i,t,e){let{startAngle:s,pixelMargin:n,x:o,y:a,outerRadius:r,innerRadius:l}=t,c=n/r;i.beginPath(),i.arc(o,a,r,s-c,e+c),l>n?(c=n/l,i.arc(o,a,l,e+c,s-c,!0)):i.arc(o,a,n,e+N,s-N),i.closePath(),i.clip()}function Vl(i){return je(i,["outerStart","outerEnd","innerStart","innerEnd"])}function Wl(i,t,e,s){let n=Vl(i.options.borderRadius),o=(e-t)/2,a=Math.min(o,s*t/2),r=l=>{let c=(e-Math.min(o,l))*s/2;return $(l,0,Math.min(o,c))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:$(n.innerStart,0,a),innerEnd:$(n.innerEnd,0,a)}}function Jt(i,t,e,s){return{x:e+i*Math.cos(t),y:s+i*Math.sin(t)}}function ri(i,t,e,s,n,o){let{x:a,y:r,startAngle:l,pixelMargin:c,innerRadius:h}=t,d=Math.max(t.outerRadius+s+e-c,0),u=h>0?h+s+e+c:0,f=0,g=n-l;if(s){let R=h>0?h-s:0,B=d>0?d-s:0,H=(R+B)/2,st=H!==0?g*H/(H+s):g;f=(g-st)/2}let p=Math.max(.001,g*d-e/I)/d,m=(g-p)/2,b=l+m+f,x=n-m-f,{outerStart:v,outerEnd:y,innerStart:_,innerEnd:M}=Wl(t,u,d,x-b),k=d-v,w=d-y,S=b+v/k,D=x-y/w,C=u+_,A=u+M,Y=b+_/C,tt=x-M/A;if(i.beginPath(),o){let R=(S+D)/2;if(i.arc(a,r,d,S,R),i.arc(a,r,d,R,D),y>0){let K=Jt(w,D,a,r);i.arc(K.x,K.y,y,D,x+N)}let B=Jt(A,x,a,r);if(i.lineTo(B.x,B.y),M>0){let K=Jt(A,tt,a,r);i.arc(K.x,K.y,M,x+N,tt+Math.PI)}let H=(x-M/u+(b+_/u))/2;if(i.arc(a,r,u,x-M/u,H,!0),i.arc(a,r,u,H,b+_/u,!0),_>0){let K=Jt(C,Y,a,r);i.arc(K.x,K.y,_,Y+Math.PI,b-N)}let st=Jt(k,b,a,r);if(i.lineTo(st.x,st.y),v>0){let K=Jt(k,S,a,r);i.arc(K.x,K.y,v,b-N,S)}}else{i.moveTo(a,r);let R=Math.cos(S)*d+a,B=Math.sin(S)*d+r;i.lineTo(R,B);let H=Math.cos(D)*d+a,st=Math.sin(D)*d+r;i.lineTo(H,st)}i.closePath()}function Nl(i,t,e,s,n){let{fullCircles:o,startAngle:a,circumference:r}=t,l=t.endAngle;if(o){ri(i,t,e,s,l,n);for(let c=0;ct!=="borderDash"};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,s){let n=this.getProps(["x","y"],s),{angle:o,distance:a}=Si(n,{x:t,y:e}),{startAngle:r,endAngle:l,innerRadius:c,outerRadius:h,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],s),u=(this.options.spacing+this.options.borderWidth)/2,g=P(d,l-r)>=z||Kt(o,r,l),p=ct(a,c+u,h+u);return g&&p}getCenterPoint(t){let{x:e,y:s,startAngle:n,endAngle:o,innerRadius:a,outerRadius:r}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:l,spacing:c}=this.options,h=(n+o)/2,d=(a+r+c+l)/2;return{x:e+Math.cos(h)*d,y:s+Math.sin(h)*d}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){let{options:e,circumference:s}=this,n=(e.offset||0)/4,o=(e.spacing||0)/2,a=e.circular;if(this.pixelMargin=e.borderAlign==="inner"?.33:0,this.fullCircles=s>z?Math.floor(s/z):0,s===0||this.innerRadius<0||this.outerRadius<0)return;t.save();let r=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(r)*n,Math.sin(r)*n);let l=1-Math.sin(Math.min(I,s||0)),c=n*l;t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,Nl(t,this,c,o,a),Hl(t,this,c,o,a),t.restore()}};function Io(i,t,e=t){i.lineCap=P(e.borderCapStyle,t.borderCapStyle),i.setLineDash(P(e.borderDash,t.borderDash)),i.lineDashOffset=P(e.borderDashOffset,t.borderDashOffset),i.lineJoin=P(e.borderJoinStyle,t.borderJoinStyle),i.lineWidth=P(e.borderWidth,t.borderWidth),i.strokeStyle=P(e.borderColor,t.borderColor)}function jl(i,t,e){i.lineTo(e.x,e.y)}function $l(i){return i.stepped?dn:i.tension||i.cubicInterpolationMode==="monotone"?un:jl}function zo(i,t,e={}){let s=i.length,{start:n=0,end:o=s-1}=e,{start:a,end:r}=t,l=Math.max(n,a),c=Math.min(o,r),h=nr&&o>r;return{count:s,start:l,loop:t.loop,ilen:c(a+(c?r-y:y))%o,v=()=>{p!==m&&(i.lineTo(h,m),i.lineTo(h,p),i.lineTo(h,b))};for(l&&(f=n[x(0)],i.moveTo(f.x,f.y)),u=0;u<=r;++u){if(f=n[x(u)],f.skip)continue;let y=f.x,_=f.y,M=y|0;M===g?(_m&&(m=_),h=(d*h+y)/++d):(v(),i.lineTo(y,_),g=M,d=0,p=m=_),b=_}v()}function ys(i){let t=i.options,e=t.borderDash&&t.borderDash.length;return!i._decimated&&!i._loop&&!t.tension&&t.cubicInterpolationMode!=="monotone"&&!t.stepped&&!e?Ul:Yl}function Xl(i){return i.stepped?vn:i.tension||i.cubicInterpolationMode==="monotone"?Mn:Mt}function Kl(i,t,e,s){let n=t._path;n||(n=t._path=new Path2D,t.path(n,e,s)&&n.closePath()),Io(i,t.options),i.stroke(n)}function ql(i,t,e,s){let{segments:n,options:o}=t,a=ys(t);for(let r of n)Io(i,o,r.style),i.beginPath(),a(i,t,r,{start:e,end:e+s-1})&&i.closePath(),i.stroke()}var Gl=typeof Path2D=="function";function Zl(i,t,e,s){Gl&&!t.options.segment?Kl(i,t,e,s):ql(i,t,e,s)}var te=class extends ot{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>t!=="borderDash"&&t!=="fill"};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){let s=this.options;if((s.tension||s.cubicInterpolationMode==="monotone")&&!s.stepped&&!this._pointsUpdated){let n=s.spanGaps?this._loop:this._fullLoop;xn(this._points,s,t,n,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=wn(this,this.options.segment))}first(){let t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){let t=this.segments,e=this.points,s=t.length;return s&&e[t[s-1].end]}interpolate(t,e){let s=this.options,n=t[e],o=this.points,a=Ui(this,{property:e,start:n,end:n});if(!a.length)return;let r=[],l=Xl(s),c,h;for(c=0,h=a.length;ci.replace("rgb(","rgba(").replace(")",", 0.5)"));function Vo(i){return ks[i%ks.length]}function Wo(i){return eo[i%eo.length]}function nc(i,t){return i.borderColor=Vo(t),i.backgroundColor=Wo(t),++t}function oc(i,t){return i.backgroundColor=i.data.map(()=>Vo(t++)),t}function ac(i,t){return i.backgroundColor=i.data.map(()=>Wo(t++)),t}function rc(i){let t=0;return(e,s)=>{let n=i.getDatasetMeta(s).controller;n instanceof ye?t=oc(e,t):n instanceof ni?t=ac(e,t):n&&(t=nc(e,t))}}function io(i){let t;for(t in i)if(i[t].borderColor||i[t].backgroundColor)return!0;return!1}function lc(i){return i&&(i.borderColor||i.backgroundColor)}var cc={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(i,t,e){if(!e.enabled)return;let{data:{datasets:s},options:n}=i.config,{elements:o}=n;if(!e.forceOverride&&(io(s)||lc(n)||o&&io(o)))return;let a=rc(i);s.forEach(a)}};function hc(i,t,e,s,n){let o=n.samples||s;if(o>=e)return i.slice(t,t+e);let a=[],r=(e-2)/(o-2),l=0,c=t+e-1,h=t,d,u,f,g,p;for(a[l++]=i[h],d=0;df&&(f=g,u=i[x],p=x);a[l++]=u,h=p}return a[l++]=i[c],a}function dc(i,t,e,s){let n=0,o=0,a,r,l,c,h,d,u,f,g,p,m=[],b=t+e-1,x=i[t].x,y=i[b].x-x;for(a=t;ap&&(p=c,u=a),n=(o*n+r.x)/++o;else{let M=a-1;if(!T(d)&&!T(u)){let k=Math.min(d,u),w=Math.max(d,u);k!==f&&k!==M&&m.push({...i[k],x:n}),w!==f&&w!==M&&m.push({...i[w],x:n})}a>0&&M!==f&&m.push(i[M]),m.push(r),h=_,o=0,g=p=c,d=u=f=a}}return m}function No(i){if(i._decimated){let t=i._data;delete i._decimated,delete i._data,Object.defineProperty(i,"data",{configurable:!0,enumerable:!0,writable:!0,value:t})}}function so(i){i.data.datasets.forEach(t=>{No(t)})}function uc(i,t){let e=t.length,s=0,n,{iScale:o}=i,{min:a,max:r,minDefined:l,maxDefined:c}=o.getUserBounds();return l&&(s=$(at(t,o.axis,a).lo,0,e-1)),c?n=$(at(t,o.axis,r).hi+1,s,e)-s:n=e-s,{start:s,count:n}}var fc={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(i,t,e)=>{if(!e.enabled){so(i);return}let s=i.width;i.data.datasets.forEach((n,o)=>{let{_data:a,indexAxis:r}=n,l=i.getDatasetMeta(o),c=a||n.data;if(Zt([r,i.options.indexAxis])==="y"||!l.controller.supportsDecimation)return;let h=i.scales[l.xAxisID];if(h.type!=="linear"&&h.type!=="time"||i.options.parsing)return;let{start:d,count:u}=uc(l,c),f=e.threshold||4*s;if(u<=f){No(n);return}T(a)&&(n._data=c,delete n.data,Object.defineProperty(n,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(p){this._data=p}}));let g;switch(e.algorithm){case"lttb":g=hc(c,d,u,s,e);break;case"min-max":g=dc(c,d,u,s);break;default:throw new Error(`Unsupported decimation algorithm '${e.algorithm}'`)}n._decimated=g})},destroy(i){so(i)}};function gc(i,t,e){let s=i.segments,n=i.points,o=t.points,a=[];for(let r of s){let{start:l,end:c}=r;c=Ts(l,c,n);let h=ws(e,n[l],n[c],r.loop);if(!t.segments){a.push({source:r,target:h,start:n[l],end:n[c]});continue}let d=Ui(t,h);for(let u of d){let f=ws(e,o[u.start],o[u.end],u.loop),g=Yi(r,n,f);for(let p of g)a.push({source:p,target:u,start:{[e]:no(h,f,"start",Math.max)},end:{[e]:no(h,f,"end",Math.min)}})}}return a}function ws(i,t,e,s){if(s)return;let n=t[i],o=e[i];return i==="angle"&&(n=G(n),o=G(o)),{property:i,start:n,end:o}}function pc(i,t){let{x:e=null,y:s=null}=i||{},n=t.points,o=[];return t.segments.forEach(({start:a,end:r})=>{r=Ts(a,r,n);let l=n[a],c=n[r];s!==null?(o.push({x:l.x,y:s}),o.push({x:c.x,y:s})):e!==null&&(o.push({x:e,y:l.y}),o.push({x:e,y:c.y}))}),o}function Ts(i,t,e){for(;t>i;t--){let s=e[t];if(!isNaN(s.x)&&!isNaN(s.y))break}return t}function no(i,t,e,s){return i&&t?s(i[e],t[e]):i?i[e]:t?t[e]:0}function Ho(i,t){let e=[],s=!1;return F(i)?(s=!0,e=i):e=pc(i,t),e.length?new te({points:e,options:{tension:0},_loop:s,_fullLoop:s}):null}function oo(i){return i&&i.fill!==!1}function mc(i,t,e){let n=i[t].fill,o=[t],a;if(!e)return n;for(;n!==!1&&o.indexOf(n)===-1;){if(!V(n))return n;if(a=i[n],!a)return!1;if(a.visible)return n;o.push(n),n=a.fill}return!1}function bc(i,t,e){let s=vc(i);if(O(s))return isNaN(s.value)?!1:s;let n=parseFloat(s);return V(n)&&Math.floor(n)===n?xc(s[0],t,n,e):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function xc(i,t,e,s){return(i==="-"||i==="+")&&(e=t+e),e===t||e<0||e>=s?!1:e}function _c(i,t){let e=null;return i==="start"?e=t.bottom:i==="end"?e=t.top:O(i)?e=t.getPixelForValue(i.value):t.getBasePixel&&(e=t.getBasePixel()),e}function yc(i,t,e){let s;return i==="start"?s=e:i==="end"?s=t.options.reverse?t.min:t.max:O(i)?s=i.value:s=t.getBaseValue(),s}function vc(i){let t=i.options,e=t.fill,s=P(e&&e.target,e);return s===void 0&&(s=!!t.backgroundColor),s===!1||s===null?!1:s===!0?"origin":s}function Mc(i){let{scale:t,index:e,line:s}=i,n=[],o=s.segments,a=s.points,r=kc(t,e);r.push(Ho({x:null,y:t.bottom},s));for(let l=0;l=0;--a){let r=n[a].$filler;r&&(r.line.updateControlPoints(o,r.axis),s&&r.fill&&es(i.ctx,r,o))}},beforeDatasetsDraw(i,t,e){if(e.drawTime!=="beforeDatasetsDraw")return;let s=i.getSortedVisibleDatasetMetas();for(let n=s.length-1;n>=0;--n){let o=s[n].$filler;oo(o)&&es(i.ctx,o,i.chartArea)}},beforeDatasetDraw(i,t,e){let s=t.meta.$filler;!oo(s)||e.drawTime!=="beforeDatasetDraw"||es(i.ctx,s,i.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}},co=(i,t)=>{let{boxHeight:e=t,boxWidth:s=t}=i;return i.usePointStyle&&(e=Math.min(e,t),s=i.pointStyleWidth||Math.min(s,t)),{boxWidth:s,boxHeight:e,itemHeight:Math.max(t,e)}},Ec=(i,t)=>i!==null&&t!==null&&i.datasetIndex===t.datasetIndex&&i.index===t.index,ci=class extends ot{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,s){this.maxWidth=t,this.maxHeight=e,this._margins=s,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){let t=this.options.labels||{},e=E(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter(s=>t.filter(s,this.chart.data))),t.sort&&(e=e.sort((s,n)=>t.sort(s,n,this.chart.data))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){let{options:t,ctx:e}=this;if(!t.display){this.width=this.height=0;return}let s=t.labels,n=j(s.font),o=n.size,a=this._computeTitleHeight(),{boxWidth:r,itemHeight:l}=co(s,o),c,h;e.font=n.string,this.isHorizontal()?(c=this.maxWidth,h=this._fitRows(a,o,r,l)+10):(h=this.maxHeight,c=this._fitCols(a,n,r,l)+10),this.width=Math.min(c,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,s,n){let{ctx:o,maxWidth:a,options:{labels:{padding:r}}}=this,l=this.legendHitBoxes=[],c=this.lineWidths=[0],h=n+r,d=t;o.textAlign="left",o.textBaseline="middle";let u=-1,f=-h;return this.legendItems.forEach((g,p)=>{let m=s+e/2+o.measureText(g.text).width;(p===0||c[c.length-1]+m+2*r>a)&&(d+=h,c[c.length-(p>0?0:1)]=0,f+=h,u++),l[p]={left:0,top:f,row:u,width:m,height:n},c[c.length-1]+=m+r}),d}_fitCols(t,e,s,n){let{ctx:o,maxHeight:a,options:{labels:{padding:r}}}=this,l=this.legendHitBoxes=[],c=this.columnSizes=[],h=a-t,d=r,u=0,f=0,g=0,p=0;return this.legendItems.forEach((m,b)=>{let{itemWidth:x,itemHeight:v}=Fc(s,e,o,m,n);b>0&&f+v+2*r>h&&(d+=u+r,c.push({width:u,height:f}),g+=u+r,p++,u=f=0),l[b]={left:g,top:f,col:p,width:x,height:v},u=Math.max(u,x),f+=v+r}),d+=u,c.push({width:u,height:f}),d}adjustHitBoxes(){if(!this.options.display)return;let t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:s,labels:{padding:n},rtl:o}}=this,a=Ft(o,this.left,this.width);if(this.isHorizontal()){let r=0,l=U(s,this.left+n,this.right-this.lineWidths[r]);for(let c of e)r!==c.row&&(r=c.row,l=U(s,this.left+n,this.right-this.lineWidths[r])),c.top+=this.top+t+n,c.left=a.leftForLtr(a.x(l),c.width),l+=c.width+n}else{let r=0,l=U(s,this.top+t+n,this.bottom-this.columnSizes[r].height);for(let c of e)c.col!==r&&(r=c.col,l=U(s,this.top+t+n,this.bottom-this.columnSizes[r].height)),c.top=l,c.left+=this.left+n,c.left=a.leftForLtr(a.x(c.left),c.width),l+=c.height+n}}isHorizontal(){return this.options.position==="top"||this.options.position==="bottom"}draw(){if(this.options.display){let t=this.ctx;de(t,this),this._draw(),ue(t)}}_draw(){let{options:t,columnSizes:e,lineWidths:s,ctx:n}=this,{align:o,labels:a}=t,r=W.color,l=Ft(t.rtl,this.left,this.width),c=j(a.font),{padding:h}=a,d=c.size,u=d/2,f;this.drawTitle(),n.textAlign=l.textAlign("left"),n.textBaseline="middle",n.lineWidth=.5,n.font=c.string;let{boxWidth:g,boxHeight:p,itemHeight:m}=co(a,d),b=function(M,k,w){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;n.save();let S=P(w.lineWidth,1);if(n.fillStyle=P(w.fillStyle,r),n.lineCap=P(w.lineCap,"butt"),n.lineDashOffset=P(w.lineDashOffset,0),n.lineJoin=P(w.lineJoin,"miter"),n.lineWidth=S,n.strokeStyle=P(w.strokeStyle,r),n.setLineDash(P(w.lineDash,[])),a.usePointStyle){let D={radius:p*Math.SQRT2/2,pointStyle:w.pointStyle,rotation:w.rotation,borderWidth:S},C=l.xPlus(M,g/2),A=k+u;Fi(n,D,C,A,a.pointStyleWidth&&g)}else{let D=k+Math.max((d-p)/2,0),C=l.leftForLtr(M,g),A=Pt(w.borderRadius);n.beginPath(),Object.values(A).some(Y=>Y!==0)?Gt(n,{x:C,y:D,w:g,h:p,radius:A}):n.rect(C,D,g,p),n.fill(),S!==0&&n.stroke()}n.restore()},x=function(M,k,w){St(n,w.text,M,k+m/2,c,{strikethrough:w.hidden,textAlign:l.textAlign(w.textAlign)})},v=this.isHorizontal(),y=this._computeTitleHeight();v?f={x:U(o,this.left+h,this.right-s[0]),y:this.top+h+y,line:0}:f={x:this.left+h,y:U(o,this.top+y+h,this.bottom-e[0].height),line:0},ji(this.ctx,t.textDirection);let _=m+h;this.legendItems.forEach((M,k)=>{n.strokeStyle=M.fontColor,n.fillStyle=M.fontColor;let w=n.measureText(M.text).width,S=l.textAlign(M.textAlign||(M.textAlign=a.textAlign)),D=g+u+w,C=f.x,A=f.y;l.setWidth(this.width),v?k>0&&C+D+h>this.right&&(A=f.y+=_,f.line++,C=f.x=U(o,this.left+h,this.right-s[f.line])):k>0&&A+_>this.bottom&&(C=f.x=C+e[f.line].width+h,f.line++,A=f.y=U(o,this.top+y+h,this.bottom-e[f.line].height));let Y=l.x(C);if(b(Y,A,M),C=ln(S,C+g+u,v?C+D:this.right,t.rtl),x(l.x(C),A,M),v)f.x+=D+h;else if(typeof M.text!="string"){let tt=c.lineHeight;f.y+=jo(M,tt)+h}else f.y+=_}),$i(this.ctx,t.textDirection)}drawTitle(){let t=this.options,e=t.title,s=j(e.font),n=X(e.padding);if(!e.display)return;let o=Ft(t.rtl,this.left,this.width),a=this.ctx,r=e.position,l=s.size/2,c=n.top+l,h,d=this.left,u=this.width;if(this.isHorizontal())u=Math.max(...this.lineWidths),h=this.top+c,d=U(t.align,d,this.right-u);else{let g=this.columnSizes.reduce((p,m)=>Math.max(p,m.height),0);h=c+U(t.align,this.top,this.bottom-g-t.labels.padding-this._computeTitleHeight())}let f=U(r,d,d+u);a.textAlign=o.textAlign(We(r)),a.textBaseline="middle",a.strokeStyle=e.color,a.fillStyle=e.color,a.font=s.string,St(a,e.text,f,h,s)}_computeTitleHeight(){let t=this.options.title,e=j(t.font),s=X(t.padding);return t.display?e.lineHeight+s.height:0}_getLegendItemAt(t,e){let s,n,o;if(ct(t,this.left,this.right)&&ct(e,this.top,this.bottom)){for(o=this.legendHitBoxes,s=0;so.length>a.length?o:a)),t+e.size/2+s.measureText(n).width}function zc(i,t,e){let s=i;return typeof t.text!="string"&&(s=jo(t,e)),s}function jo(i,t){let e=i.text?i.text.length:0;return t*e}function Bc(i,t){return!!((i==="mousemove"||i==="mouseout")&&(t.onHover||t.onLeave)||t.onClick&&(i==="click"||i==="mouseup"))}var Vc={id:"legend",_element:ci,start(i,t,e){let s=i.legend=new ci({ctx:i.ctx,options:e,chart:i});q.configure(i,s,e),q.addBox(i,s)},stop(i){q.removeBox(i,i.legend),delete i.legend},beforeUpdate(i,t,e){let s=i.legend;q.configure(i,s,e),s.options=e},afterUpdate(i){let t=i.legend;t.buildLabels(),t.adjustHitBoxes()},afterEvent(i,t){t.replay||i.legend.handleEvent(t.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(i,t,e){let s=t.datasetIndex,n=e.chart;n.isDatasetVisible(s)?(n.hide(s),t.hidden=!0):(n.show(s),t.hidden=!1)},onHover:null,onLeave:null,labels:{color:i=>i.chart.options.color,boxWidth:40,padding:10,generateLabels(i){let t=i.data.datasets,{labels:{usePointStyle:e,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=i.legend.options;return i._getSortedDatasetMetas().map(l=>{let c=l.controller.getStyle(e?0:void 0),h=X(c.borderWidth);return{text:t[l.index].label,fillStyle:c.backgroundColor,fontColor:o,hidden:!l.visible,lineCap:c.borderCapStyle,lineDash:c.borderDash,lineDashOffset:c.borderDashOffset,lineJoin:c.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:c.borderColor,pointStyle:s||c.pointStyle,rotation:c.rotation,textAlign:n||c.textAlign,borderRadius:a&&(r||c.borderRadius),datasetIndex:l.index}},this)}},title:{color:i=>i.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:i=>!i.startsWith("on"),labels:{_scriptable:i=>!["generateLabels","filter","sort"].includes(i)}}},Me=class extends ot{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){let s=this.options;if(this.left=0,this.top=0,!s.display){this.width=this.height=this.right=this.bottom=0;return}this.width=this.right=t,this.height=this.bottom=e;let n=F(s.text)?s.text.length:1;this._padding=X(s.padding);let o=n*j(s.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){let t=this.options.position;return t==="top"||t==="bottom"}_drawArgs(t){let{top:e,left:s,bottom:n,right:o,options:a}=this,r=a.align,l=0,c,h,d;return this.isHorizontal()?(h=U(r,s,o),d=e+t,c=o-s):(a.position==="left"?(h=s+t,d=U(r,n,e),l=I*-.5):(h=o-t,d=U(r,e,n),l=I*.5),c=n-e),{titleX:h,titleY:d,maxWidth:c,rotation:l}}draw(){let t=this.ctx,e=this.options;if(!e.display)return;let s=j(e.font),o=s.lineHeight/2+this._padding.top,{titleX:a,titleY:r,maxWidth:l,rotation:c}=this._drawArgs(o);St(t,e.text,0,0,s,{color:e.color,maxWidth:l,rotation:c,textAlign:We(e.align),textBaseline:"middle",translation:[a,r]})}};function Wc(i,t){let e=new Me({ctx:i.ctx,options:t,chart:i});q.configure(i,e,t),q.addBox(i,e),i.titleBlock=e}var Nc={id:"title",_element:Me,start(i,t,e){Wc(i,e)},stop(i){let t=i.titleBlock;q.removeBox(i,t),delete i.titleBlock},beforeUpdate(i,t,e){let s=i.titleBlock;q.configure(i,s,e),s.options=e},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},Je=new WeakMap,Hc={id:"subtitle",start(i,t,e){let s=new Me({ctx:i.ctx,options:e,chart:i});q.configure(i,s,e),q.addBox(i,s),Je.set(i,s)},stop(i){q.removeBox(i,Je.get(i)),Je.delete(i)},beforeUpdate(i,t,e){let s=Je.get(i);q.configure(i,s,e),s.options=e},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},_e={average(i){if(!i.length)return!1;let t,e,s=new Set,n=0,o=0;for(t=0,e=i.length;tr+l)/s.size,y:n/o}},nearest(i,t){if(!i.length)return!1;let e=t.x,s=t.y,n=Number.POSITIVE_INFINITY,o,a,r;for(o=0,a=i.length;o-1?i.split(` +`):i}function jc(i,t){let{element:e,datasetIndex:s,index:n}=t,o=i.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:i,label:a,parsed:o.getParsed(n),raw:i.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:e}}function ho(i,t){let e=i.chart.ctx,{body:s,footer:n,title:o}=i,{boxWidth:a,boxHeight:r}=t,l=j(t.bodyFont),c=j(t.titleFont),h=j(t.footerFont),d=o.length,u=n.length,f=s.length,g=X(t.padding),p=g.height,m=0,b=s.reduce((y,_)=>y+_.before.length+_.lines.length+_.after.length,0);if(b+=i.beforeBody.length+i.afterBody.length,d&&(p+=d*c.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),b){let y=t.displayColors?Math.max(r,l.lineHeight):l.lineHeight;p+=f*y+(b-f)*l.lineHeight+(b-1)*t.bodySpacing}u&&(p+=t.footerMarginTop+u*h.lineHeight+(u-1)*t.footerSpacing);let x=0,v=function(y){m=Math.max(m,e.measureText(y).width+x)};return e.save(),e.font=c.string,L(i.title,v),e.font=l.string,L(i.beforeBody.concat(i.afterBody),v),x=t.displayColors?a+2+t.boxPadding:0,L(s,y=>{L(y.before,v),L(y.lines,v),L(y.after,v)}),x=0,e.font=h.string,L(i.footer,v),e.restore(),m+=g.width,{width:m,height:p}}function $c(i,t){let{y:e,height:s}=t;return ei.height-s/2?"bottom":"center"}function Yc(i,t,e,s){let{x:n,width:o}=s,a=e.caretSize+e.caretPadding;if(i==="left"&&n+o+a>t.width||i==="right"&&n-o-a<0)return!0}function Uc(i,t,e,s){let{x:n,width:o}=e,{width:a,chartArea:{left:r,right:l}}=i,c="center";return s==="center"?c=n<=(r+l)/2?"left":"right":n<=o/2?c="left":n>=a-o/2&&(c="right"),Yc(c,i,t,e)&&(c="center"),c}function uo(i,t,e){let s=e.yAlign||t.yAlign||$c(i,e);return{xAlign:e.xAlign||t.xAlign||Uc(i,t,e,s),yAlign:s}}function Xc(i,t){let{x:e,width:s}=i;return t==="right"?e-=s:t==="center"&&(e-=s/2),e}function Kc(i,t,e){let{y:s,height:n}=i;return t==="top"?s+=e:t==="bottom"?s-=n+e:s-=n/2,s}function fo(i,t,e,s){let{caretSize:n,caretPadding:o,cornerRadius:a}=i,{xAlign:r,yAlign:l}=e,c=n+o,{topLeft:h,topRight:d,bottomLeft:u,bottomRight:f}=Pt(a),g=Xc(t,r),p=Kc(t,l,c);return l==="center"?r==="left"?g+=c:r==="right"&&(g-=c):r==="left"?g-=Math.max(h,u)+n:r==="right"&&(g+=Math.max(d,f)+n),{x:$(g,0,s.width-t.width),y:$(p,0,s.height-t.height)}}function Qe(i,t,e){let s=X(e.padding);return t==="center"?i.x+i.width/2:t==="right"?i.x+i.width-s.right:i.x+s.left}function go(i){return ht([],_t(i))}function qc(i,t,e){return bt(i,{tooltip:t,tooltipItems:e,type:"tooltip"})}function po(i,t){let e=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return e?i.override(e):i}var $o={beforeTitle:lt,title(i){if(i.length>0){let t=i[0],e=t.chart.data.labels,s=e?e.length:0;if(this&&this.options&&this.options.mode==="dataset")return t.dataset.label||"";if(t.label)return t.label;if(s>0&&t.dataIndex"u"?$o[t].call(e,s):n}var hi=class extends ot{static positioners=_e;constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let t=this._cachedAnimations;if(t)return t;let e=this.chart,s=this.options.setContext(this.getContext()),n=s.enabled&&e.options.animation&&s.animations,o=new si(this.chart,n);return n._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=qc(this.chart.getContext(),this,this._tooltipItems))}getTitle(t,e){let{callbacks:s}=e,n=J(s,"beforeTitle",this,t),o=J(s,"title",this,t),a=J(s,"afterTitle",this,t),r=[];return r=ht(r,_t(n)),r=ht(r,_t(o)),r=ht(r,_t(a)),r}getBeforeBody(t,e){return go(J(e.callbacks,"beforeBody",this,t))}getBody(t,e){let{callbacks:s}=e,n=[];return L(t,o=>{let a={before:[],lines:[],after:[]},r=po(s,o);ht(a.before,_t(J(r,"beforeLabel",this,o))),ht(a.lines,J(r,"label",this,o)),ht(a.after,_t(J(r,"afterLabel",this,o))),n.push(a)}),n}getAfterBody(t,e){return go(J(e.callbacks,"afterBody",this,t))}getFooter(t,e){let{callbacks:s}=e,n=J(s,"beforeFooter",this,t),o=J(s,"footer",this,t),a=J(s,"afterFooter",this,t),r=[];return r=ht(r,_t(n)),r=ht(r,_t(o)),r=ht(r,_t(a)),r}_createItems(t){let e=this._active,s=this.chart.data,n=[],o=[],a=[],r=[],l,c;for(l=0,c=e.length;lt.filter(h,d,u,s))),t.itemSort&&(r=r.sort((h,d)=>t.itemSort(h,d,s))),L(r,h=>{let d=po(t.callbacks,h);n.push(J(d,"labelColor",this,h)),o.push(J(d,"labelPointStyle",this,h)),a.push(J(d,"labelTextColor",this,h))}),this.labelColors=n,this.labelPointStyles=o,this.labelTextColors=a,this.dataPoints=r,r}update(t,e){let s=this.options.setContext(this.getContext()),n=this._active,o,a=[];if(!n.length)this.opacity!==0&&(o={opacity:0});else{let r=_e[s.position].call(this,n,this._eventPosition);a=this._createItems(s),this.title=this.getTitle(a,s),this.beforeBody=this.getBeforeBody(a,s),this.body=this.getBody(a,s),this.afterBody=this.getAfterBody(a,s),this.footer=this.getFooter(a,s);let l=this._size=ho(this,s),c=Object.assign({},r,l),h=uo(this.chart,s,c),d=fo(s,c,h,this.chart);this.xAlign=h.xAlign,this.yAlign=h.yAlign,o={opacity:1,x:d.x,y:d.y,width:l.width,height:l.height,caretX:r.x,caretY:r.y}}this._tooltipItems=a,this.$context=void 0,o&&this._resolveAnimations().update(this,o),t&&s.external&&s.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,s,n){let o=this.getCaretPosition(t,s,n);e.lineTo(o.x1,o.y1),e.lineTo(o.x2,o.y2),e.lineTo(o.x3,o.y3)}getCaretPosition(t,e,s){let{xAlign:n,yAlign:o}=this,{caretSize:a,cornerRadius:r}=s,{topLeft:l,topRight:c,bottomLeft:h,bottomRight:d}=Pt(r),{x:u,y:f}=t,{width:g,height:p}=e,m,b,x,v,y,_;return o==="center"?(y=f+p/2,n==="left"?(m=u,b=m-a,v=y+a,_=y-a):(m=u+g,b=m+a,v=y-a,_=y+a),x=m):(n==="left"?b=u+Math.max(l,h)+a:n==="right"?b=u+g-Math.max(c,d)-a:b=this.caretX,o==="top"?(v=f,y=v-a,m=b-a,x=b+a):(v=f+p,y=v+a,m=b+a,x=b-a),_=v),{x1:m,x2:b,x3:x,y1:v,y2:y,y3:_}}drawTitle(t,e,s){let n=this.title,o=n.length,a,r,l;if(o){let c=Ft(s.rtl,this.x,this.width);for(t.x=Qe(this,s.titleAlign,s),e.textAlign=c.textAlign(s.titleAlign),e.textBaseline="middle",a=j(s.titleFont),r=s.titleSpacing,e.fillStyle=s.titleColor,e.font=a.string,l=0;lx!==0)?(t.beginPath(),t.fillStyle=o.multiKeyBackground,Gt(t,{x:p,y:g,w:c,h:l,radius:b}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),Gt(t,{x:m,y:g+1,w:c-2,h:l-2,radius:b}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(p,g,c,l),t.strokeRect(p,g,c,l),t.fillStyle=a.backgroundColor,t.fillRect(m,g+1,c-2,l-2))}t.fillStyle=this.labelTextColors[s]}drawBody(t,e,s){let{body:n}=this,{bodySpacing:o,bodyAlign:a,displayColors:r,boxHeight:l,boxWidth:c,boxPadding:h}=s,d=j(s.bodyFont),u=d.lineHeight,f=0,g=Ft(s.rtl,this.x,this.width),p=function(w){e.fillText(w,g.x(t.x+f),t.y+u/2),t.y+=u+o},m=g.textAlign(a),b,x,v,y,_,M,k;for(e.textAlign=a,e.textBaseline="middle",e.font=d.string,t.x=Qe(this,m,s),e.fillStyle=s.bodyColor,L(this.beforeBody,p),f=r&&m!=="right"?a==="center"?c/2+h:c+2+h:0,y=0,M=n.length;y0&&e.stroke()}_updateAnimationTarget(t){let e=this.chart,s=this.$animations,n=s&&s.x,o=s&&s.y;if(n||o){let a=_e[t.position].call(this,this._active,this._eventPosition);if(!a)return;let r=this._size=ho(this,t),l=Object.assign({},a,this._size),c=uo(e,t,l),h=fo(t,l,c,e);(n._to!==h.x||o._to!==h.y)&&(this.xAlign=c.xAlign,this.yAlign=c.yAlign,this.width=r.width,this.height=r.height,this.caretX=a.x,this.caretY=a.y,this._resolveAnimations().update(this,h))}}_willRender(){return!!this.opacity}draw(t){let e=this.options.setContext(this.getContext()),s=this.opacity;if(!s)return;this._updateAnimationTarget(e);let n={width:this.width,height:this.height},o={x:this.x,y:this.y};s=Math.abs(s)<.001?0:s;let a=X(e.padding),r=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&r&&(t.save(),t.globalAlpha=s,this.drawBackground(o,t,n,e),ji(t,e.textDirection),o.y+=a.top,this.drawTitle(o,t,e),this.drawBody(o,t,e),this.drawFooter(o,t,e),$i(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){let s=this._active,n=t.map(({datasetIndex:r,index:l})=>{let c=this.chart.getDatasetMeta(r);if(!c)throw new Error("Cannot find a dataset at index "+r);return{datasetIndex:r,element:c.data[l],index:l}}),o=!ce(s,n),a=this._positionChanged(n,e);(o||a)&&(this._active=n,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,s=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;let n=this.options,o=this._active||[],a=this._getActiveElements(t,o,e,s),r=this._positionChanged(a,t),l=e||!ce(a,o)||r;return l&&(this._active=a,(n.enabled||n.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),l}_getActiveElements(t,e,s,n){let o=this.options;if(t.type==="mouseout")return[];if(!n)return e.filter(r=>this.chart.data.datasets[r.datasetIndex]&&this.chart.getDatasetMeta(r.datasetIndex).controller.getParsed(r.index)!==void 0);let a=this.chart.getElementsAtEventForMode(t,o.mode,o,s);return o.reverse&&a.reverse(),a}_positionChanged(t,e){let{caretX:s,caretY:n,options:o}=this,a=_e[o.position].call(this,t,e);return a!==!1&&(s!==a.x||n!==a.y)}},Gc={id:"tooltip",_element:hi,positioners:_e,afterInit(i,t,e){e&&(i.tooltip=new hi({chart:i,options:e}))},beforeUpdate(i,t,e){i.tooltip&&i.tooltip.initialize(e)},reset(i,t,e){i.tooltip&&i.tooltip.initialize(e)},afterDraw(i){let t=i.tooltip;if(t&&t._willRender()){let e={tooltip:t};if(i.notifyPlugins("beforeTooltipDraw",{...e,cancelable:!0})===!1)return;t.draw(i.ctx),i.notifyPlugins("afterTooltipDraw",e)}},afterEvent(i,t){if(i.tooltip){let e=t.replay;i.tooltip.handleEvent(t.event,e,t.inChartArea)&&(t.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(i,t)=>t.bodyFont.size,boxWidth:(i,t)=>t.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:$o},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:i=>i!=="filter"&&i!=="itemSort"&&i!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},Zc=Object.freeze({__proto__:null,Colors:cc,Decimation:fc,Filler:Rc,Legend:Vc,SubTitle:Hc,Title:Nc,Tooltip:Gc}),Jc=(i,t,e,s)=>(typeof t=="string"?(e=i.push(t)-1,s.unshift({index:e,label:t})):isNaN(t)&&(e=null),e);function Qc(i,t,e,s){let n=i.indexOf(t);if(n===-1)return Jc(i,t,e,s);let o=i.lastIndexOf(t);return n!==o?e:n}var th=(i,t)=>i===null?null:$(Math.round(i),0,t);function mo(i){let t=this.getLabels();return i>=0&&ie.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}};function eh(i,t){let e=[],{bounds:n,step:o,min:a,max:r,precision:l,count:c,maxTicks:h,maxDigits:d,includeBounds:u}=i,f=o||1,g=h-1,{min:p,max:m}=t,b=!T(a),x=!T(r),v=!T(c),y=(m-p)/(d+1),_=Mi((m-p)/g/f)*f,M,k,w,S;if(_<1e-14&&!b&&!x)return[{value:p},{value:m}];S=Math.ceil(m/_)-Math.floor(p/_),S>g&&(_=Mi(S*_/g/f)*f),T(l)||(M=Math.pow(10,l),_=Math.ceil(_*M)/M),n==="ticks"?(k=Math.floor(p/_)*_,w=Math.ceil(m/_)*_):(k=p,w=m),b&&x&&o&&tn((r-a)/o,_/1e3)?(S=Math.round(Math.min((r-a)/_,h)),_=(r-a)/S,k=a,w=r):v?(k=b?a:k,w=x?r:w,S=c-1,_=(w-k)/S):(S=(w-k)/_,Xt(S,Math.round(S),_/1e3)?S=Math.round(S):S=Math.ceil(S));let D=Math.max(wi(_),wi(k));M=Math.pow(10,T(l)?D:l),k=Math.round(k*M)/M,w=Math.round(w*M)/M;let C=0;for(b&&(u&&k!==a?(e.push({value:a}),kr)break;e.push({value:A})}return x&&u&&w!==r?e.length&&Xt(e[e.length-1].value,r,bo(r,y,i))?e[e.length-1].value=r:e.push({value:r}):(!x||w===r)&&e.push({value:w}),e}function bo(i,t,{horizontal:e,minRotation:s}){let n=it(s),o=(e?Math.sin(n):Math.cos(n))||.001,a=.75*t*(""+i).length;return Math.min(t/o,a)}var ee=class extends Bt{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return T(t)||(typeof t=="number"||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){let{beginAtZero:t}=this.options,{minDefined:e,maxDefined:s}=this.getUserBounds(),{min:n,max:o}=this,a=l=>n=e?n:l,r=l=>o=s?o:l;if(t){let l=nt(n),c=nt(o);l<0&&c<0?r(0):l>0&&c>0&&a(0)}if(n===o){let l=o===0?1:Math.abs(o*.05);r(o+l),t||a(n-l)}this.min=n,this.max=o}getTickLimit(){let t=this.options.ticks,{maxTicksLimit:e,stepSize:s}=t,n;return s?(n=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,n>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${n} ticks. Limiting to 1000.`),n=1e3)):(n=this.computeTickLimit(),e=e||11),e&&(n=Math.min(e,n)),n}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){let t=this.options,e=t.ticks,s=this.getTickLimit();s=Math.max(2,s);let n={maxTicks:s,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:e.includeBounds!==!1},o=this._range||this,a=eh(n,o);return t.bounds==="ticks"&&ki(a,this,"value"),t.reverse?(a.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),a}configure(){let t=this.ticks,e=this.min,s=this.max;if(super.configure(),this.options.offset&&t.length){let n=(s-e)/Math.max(t.length-1,1)/2;e-=n,s+=n}this._startValue=e,this._endValue=s,this._valueRange=s-e}getLabelForValue(t){return qt(t,this.chart.options.locale,this.options.ticks.format)}},Ps=class extends ee{static id="linear";static defaults={ticks:{callback:he.formatters.numeric}};determineDataLimits(){let{min:t,max:e}=this.getMinMax(!0);this.min=V(t)?t:0,this.max=V(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){let t=this.isHorizontal(),e=t?this.width:this.height,s=it(this.options.ticks.minRotation),n=(t?Math.sin(s):Math.cos(s))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,o.lineHeight/n))}getPixelForValue(t){return t===null?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}},ke=i=>Math.floor(pt(i)),zt=(i,t)=>Math.pow(10,ke(i)+t);function xo(i){return i/Math.pow(10,ke(i))===1}function _o(i,t,e){let s=Math.pow(10,e),n=Math.floor(i/s);return Math.ceil(t/s)-n}function ih(i,t){let e=t-i,s=ke(e);for(;_o(i,t,s)>10;)s++;for(;_o(i,t,s)<10;)s--;return Math.min(s,ke(i))}function sh(i,{min:t,max:e}){t=Z(i.min,t);let s=[],n=ke(t),o=ih(t,e),a=o<0?Math.pow(10,Math.abs(o)):1,r=Math.pow(10,o),l=n>o?Math.pow(10,n):0,c=Math.round((t-l)*a)/a,h=Math.floor((t-l)/r/10)*r*10,d=Math.floor((c-h)/Math.pow(10,o)),u=Z(i.min,Math.round((l+h+d*Math.pow(10,o))*a)/a);for(;u=10?d=d<15?15:20:d++,d>=20&&(o++,d=2,a=o>=0?1:a),u=Math.round((l+h+d*Math.pow(10,o))*a)/a;let f=Z(i.max,u);return s.push({value:f,major:xo(f),significand:d}),s}var Ds=class extends Bt{static id="logarithmic";static defaults={ticks:{callback:he.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){let s=ee.prototype.parse.apply(this,[t,e]);if(s===0){this._zero=!0;return}return V(s)&&s>0?s:null}determineDataLimits(){let{min:t,max:e}=this.getMinMax(!0);this.min=V(t)?Math.max(0,t):null,this.max=V(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!V(this._userMin)&&(this.min=t===zt(this.min,0)?zt(this.min,-1):zt(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){let{minDefined:t,maxDefined:e}=this.getUserBounds(),s=this.min,n=this.max,o=r=>s=t?s:r,a=r=>n=e?n:r;s===n&&(s<=0?(o(1),a(10)):(o(zt(s,-1)),a(zt(n,1)))),s<=0&&o(zt(n,-1)),n<=0&&a(zt(s,1)),this.min=s,this.max=n}buildTicks(){let t=this.options,e={min:this._userMin,max:this._userMax},s=sh(e,this);return t.bounds==="ticks"&&ki(s,this,"value"),t.reverse?(s.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),s}getLabelForValue(t){return t===void 0?"0":qt(t,this.chart.options.locale,this.options.ticks.format)}configure(){let t=this.min;super.configure(),this._startValue=pt(t),this._valueRange=pt(this.max)-pt(t)}getPixelForValue(t){return(t===void 0||t===0)&&(t=this.min),t===null||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(pt(t)-this._startValue)/this._valueRange)}getValueForPixel(t){let e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}};function Cs(i){let t=i.ticks;if(t.display&&i.display){let e=X(t.backdropPadding);return P(t.font&&t.font.size,W.font.size)+e.height}return 0}function nh(i,t,e){return e=F(e)?e:[e],{w:hn(i,t.string,e),h:e.length*t.lineHeight}}function yo(i,t,e,s,n){return i===s||i===n?{start:t-e/2,end:t+e/2}:in?{start:t-e,end:t}:{start:t,end:t+e}}function oh(i){let t={l:i.left+i._padding.left,r:i.right-i._padding.right,t:i.top+i._padding.top,b:i.bottom-i._padding.bottom},e=Object.assign({},t),s=[],n=[],o=i._pointLabels.length,a=i.options.pointLabels,r=a.centerPointLabels?I/o:0;for(let l=0;lt.r&&(r=(s.end-t.r)/o,i.r=Math.max(i.r,t.r+r)),n.startt.b&&(l=(n.end-t.b)/a,i.b=Math.max(i.b,t.b+l))}function rh(i,t,e){let s=i.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=e,l=i.getPointPosition(t,s+n+a,o),c=Math.round(Be(G(l.angle+N))),h=uh(l.y,r.h,c),d=hh(c),u=dh(l.x,r.w,d);return{visible:!0,x:l.x,y:h,textAlign:d,left:u,top:h,right:u+r.w,bottom:h+r.h}}function lh(i,t){if(!t)return!0;let{left:e,top:s,right:n,bottom:o}=i;return!(rt({x:e,y:s},t)||rt({x:e,y:o},t)||rt({x:n,y:s},t)||rt({x:n,y:o},t))}function ch(i,t,e){let s=[],n=i._pointLabels.length,o=i.options,{centerPointLabels:a,display:r}=o.pointLabels,l={extra:Cs(o)/2,additionalAngle:a?I/n:0},c;for(let h=0;h270||e<90)&&(i-=t),i}function fh(i,t,e){let{left:s,top:n,right:o,bottom:a}=e,{backdropColor:r}=t;if(!T(r)){let l=Pt(t.borderRadius),c=X(t.backdropPadding);i.fillStyle=r;let h=s-c.left,d=n-c.top,u=o-s+c.width,f=a-n+c.height;Object.values(l).some(g=>g!==0)?(i.beginPath(),Gt(i,{x:h,y:d,w:u,h:f,radius:l}),i.fill()):i.fillRect(h,d,u,f)}}function gh(i,t){let{ctx:e,options:{pointLabels:s}}=i;for(let n=t-1;n>=0;n--){let o=i._pointLabelItems[n];if(!o.visible)continue;let a=s.setContext(i.getPointLabelContext(n));fh(e,a,o);let r=j(a.font),{x:l,y:c,textAlign:h}=o;St(e,i._pointLabels[n],l,c+r.lineHeight/2,r,{color:a.color,textAlign:h,textBaseline:"middle"})}}function Yo(i,t,e,s){let{ctx:n}=i;if(e)n.arc(i.xCenter,i.yCenter,t,0,z);else{let o=i.getPointPosition(0,t);n.moveTo(o.x,o.y);for(let a=1;a{let n=E(this.options.pointLabels.callback,[e,s],this);return n||n===0?n:""}).filter((e,s)=>this.chart.getDataVisibility(s))}fit(){let t=this.options;t.display&&t.pointLabels.display?oh(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,s,n){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((s-n)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,s,n))}getIndexAngle(t){let e=z/(this._pointLabels.length||1),s=this.options.startAngle||0;return G(t*e+it(s))}getDistanceFromCenterForValue(t){if(T(t))return NaN;let e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(T(t))return NaN;let e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){let e=this._pointLabels||[];if(t>=0&&t{if(d!==0||d===0&&this.min<0){l=this.getDistanceFromCenterForValue(h.value);let u=this.getContext(d),f=n.setContext(u),g=o.setContext(u);ph(this,f,l,a,g)}}),s.display){for(t.save(),r=a-1;r>=0;r--){let h=s.setContext(this.getPointLabelContext(r)),{color:d,lineWidth:u}=h;!u||!d||(t.lineWidth=u,t.strokeStyle=d,t.setLineDash(h.borderDash),t.lineDashOffset=h.borderDashOffset,l=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),c=this.getPointPosition(r,l),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(c.x,c.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){let t=this.ctx,e=this.options,s=e.ticks;if(!s.display)return;let n=this.getIndexAngle(0),o,a;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(n),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach((r,l)=>{if(l===0&&this.min>=0&&!e.reverse)return;let c=s.setContext(this.getContext(l)),h=j(c.font);if(o=this.getDistanceFromCenterForValue(this.ticks[l].value),c.showLabelBackdrop){t.font=h.string,a=t.measureText(r.label).width,t.fillStyle=c.backdropColor;let d=X(c.backdropPadding);t.fillRect(-a/2-d.left,-o-h.size/2-d.top,a+d.width,h.size+d.height)}St(t,r.label,0,-o,h,{color:c.color,strokeColor:c.textStrokeColor,strokeWidth:c.textStrokeWidth})}),t.restore()}drawTitle(){}},di={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Q=Object.keys(di);function vo(i,t){return i-t}function Mo(i,t){if(T(t))return null;let e=i._adapter,{parser:s,round:n,isoWeekday:o}=i._parseOpts,a=t;return typeof s=="function"&&(a=s(a)),V(a)||(a=typeof s=="string"?e.parse(a,s):e.parse(a)),a===null?null:(n&&(a=n==="week"&&(Et(o)||o===!0)?e.startOf(a,"isoWeek",o):e.startOf(a,n)),+a)}function ko(i,t,e,s){let n=Q.length;for(let o=Q.indexOf(i);o=Q.indexOf(e);o--){let a=Q[o];if(di[a].common&&i._adapter.diff(n,s,a)>=t-1)return a}return Q[e?Q.indexOf(e):0]}function xh(i){for(let t=Q.indexOf(i)+1,e=Q.length;t=t?e[s]:e[n];i[o]=!0}}function _h(i,t,e,s){let n=i._adapter,o=+n.startOf(t[0].value,s),a=t[t.length-1].value,r,l;for(r=o;r<=a;r=+n.add(r,1,s))l=e[r],l>=0&&(t[l].major=!0);return t}function So(i,t,e){let s=[],n={},o=t.length,a,r;for(a=0;a+t.value))}initOffsets(t=[]){let e=0,s=0,n,o;this.options.offset&&t.length&&(n=this.getDecimalForValue(t[0]),t.length===1?e=1-n:e=(this.getDecimalForValue(t[1])-n)/2,o=this.getDecimalForValue(t[t.length-1]),t.length===1?s=o:s=(o-this.getDecimalForValue(t[t.length-2]))/2);let a=t.length<3?.5:.25;e=$(e,0,a),s=$(s,0,a),this._offsets={start:e,end:s,factor:1/(e+1+s)}}_generate(){let t=this._adapter,e=this.min,s=this.max,n=this.options,o=n.time,a=o.unit||ko(o.minUnit,e,s,this._getLabelCapacity(e)),r=P(n.ticks.stepSize,1),l=a==="week"?o.isoWeekday:!1,c=Et(l)||l===!0,h={},d=e,u,f;if(c&&(d=+t.startOf(d,"isoWeek",l)),d=+t.startOf(d,c?"day":a),t.diff(s,e,a)>1e5*r)throw new Error(e+" and "+s+" are too far apart with stepSize of "+r+" "+a);let g=n.ticks.source==="data"&&this.getDataTimestamps();for(u=d,f=0;u+p)}getLabelForValue(t){let e=this._adapter,s=this.options.time;return s.tooltipFormat?e.format(t,s.tooltipFormat):e.format(t,s.displayFormats.datetime)}format(t,e){let n=this.options.time.displayFormats,o=this._unit,a=e||n[o];return this._adapter.format(t,a)}_tickFormatFunction(t,e,s,n){let o=this.options,a=o.ticks.callback;if(a)return E(a,[t,e,s],this);let r=o.time.displayFormats,l=this._unit,c=this._majorUnit,h=l&&r[l],d=c&&r[c],u=s[e],f=c&&d&&u&&u.major;return this._adapter.format(t,n||(f?d:h))}generateTickLabels(t){let e,s,n;for(e=0,s=t.length;e0?r:1}getDataTimestamps(){let t=this._cache.data||[],e,s;if(t.length)return t;let n=this.getMatchingVisibleMetas();if(this._normalized&&n.length)return this._cache.data=n[0].controller.getAllParsedValues(this);for(e=0,s=n.length;e=i[s].pos&&t<=i[n].pos&&({lo:s,hi:n}=at(i,"pos",t)),{pos:o,time:r}=i[s],{pos:a,time:l}=i[n]):(t>=i[s].time&&t<=i[n].time&&({lo:s,hi:n}=at(i,"time",t)),{time:o,pos:r}=i[s],{time:a,pos:l}=i[n]);let c=a-o;return c?r+(l-r)*(t-o)/c:r}var As=class extends we{static id="timeseries";static defaults=we.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){let t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=ti(e,this.min),this._tableRange=ti(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){let{min:e,max:s}=this,n=[],o=[],a,r,l,c,h;for(a=0,r=t.length;a=e&&c<=s&&n.push(c);if(n.length<2)return[{time:e,pos:0},{time:s,pos:1}];for(a=0,r=n.length;an-o)}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;let e=this.getDataTimestamps(),s=this.getLabelTimestamps();return e.length&&s.length?t=this.normalize(e.concat(s)):t=e.length?e:s,t=this._cache.all=t,t}getDecimalForValue(t){return(ti(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){let e=this._offsets,s=this.getDecimalForPixel(t)/e.factor-e.end;return ti(this._table,s*this._tableRange+this._minPos,!0)}},yh=Object.freeze({__proto__:null,CategoryScale:Ss,LinearScale:Ps,LogarithmicScale:Ds,RadialLinearScale:Os,TimeScale:we,TimeSeriesScale:As}),Sh=[Lr,sc,Zc,yh];export{ss as Animation,si as Animations,_s as ArcElement,ns as BarController,Ms as BarElement,oi as BasePlatform,ds as BasicPlatform,os as BubbleController,Ss as CategoryScale,xs as Chart,cc as Colors,ut as DatasetController,fc as Decimation,us as DomPlatform,ye as DoughnutController,ot as Element,Rc as Filler,Br as Interaction,Vc as Legend,as as LineController,te as LineElement,Ps as LinearScale,Ds as LogarithmicScale,rs as PieController,vs as PointElement,ni as PolarAreaController,ls as RadarController,Os as RadialLinearScale,Bt as Scale,cs as ScatterController,Hc as SubTitle,he as Ticks,we as TimeScale,As as TimeSeriesScale,Nc as Title,Gc as Tooltip,Rr as _adapters,sl as _detectPlatform,xt as animator,Lr as controllers,W as defaults,sc as elements,q as layouts,Zc as plugins,Sh as registerables,dt as registry,yh as scales}; +/*! Bundled license information: + +@kurkle/color/dist/color.esm.js: + (*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + *) + +chart.js/dist/chunks/helpers.segment.js: + (*! + * Chart.js v4.4.2 + * https://www.chartjs.org + * (c) 2024 Chart.js Contributors + * Released under the MIT License + *) + +chart.js/dist/chart.js: + (*! + * Chart.js v4.4.2 + * https://www.chartjs.org + * (c) 2024 Chart.js Contributors + * Released under the MIT License + *) +*/ diff --git a/Resources/Public/JavaScript/chart-initializer.js b/Resources/Public/JavaScript/chart-initializer.js new file mode 100644 index 0000000..f07ec91 --- /dev/null +++ b/Resources/Public/JavaScript/chart-initializer.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{Chart as t,ArcElement as i,LineElement as c,BarElement as d,PointElement as s,BarController as g,BubbleController as f,DoughnutController as u,LineController as h,PieController as C,PolarAreaController as m,RadarController as p,ScatterController as S,CategoryScale as b,LinearScale as v,LogarithmicScale as w,RadialLinearScale as y,TimeScale as E,TimeSeriesScale as L,Decimation as T,Filler as k,Legend as x,Title as z,Tooltip as D,SubTitle as R}from"chart.js";import B from"@typo3/core/event/regular-event.js";import{DashboardWidgetContentRenderedEvent as M}from"@typo3/dashboard/dashboard.js";class P{constructor(){this.selector=".dashboard-item",this.initialize()}initialize(){t.register(i,c,d,s,g,f,u,h,C,m,p,S,b,v,w,y,E,L,T,k,x,z,D,R),new B(M.eventName,(r,a)=>{r.preventDefault();const e=r.widget.eventdata;if(e===void 0||e.graphConfig===void 0)return;const l=a.querySelector("canvas");let o;if(l!==null&&(o=l.getContext("2d")),o===void 0)return;this.darkModeEnabled()?(e.graphConfig.options.color="#ccc",e.graphConfig.options.borderColor="#000",t.defaults.borderColor="rgba(255,255,255,.1)",t.defaults.color="#ccc"):(e.graphConfig.options.color="#666",e.graphConfig.options.borderColor="#fff",t.defaults.borderColor="rgba(0,0,0,.1)",t.defaults.color="#666");const n=t.getChart(o);if(n){n.data=e.graphConfig.data,n.options=e.graphConfig.options,n.update();return}new t(o,e.graphConfig)}).delegateTo(document,this.selector)}darkModeEnabled(){const r=document.querySelector(this.selector),e=window.getComputedStyle(r).colorScheme;return e==="light only"||e==="light"?!1:e==="dark only"||e==="dark"?!0:window.matchMedia("(prefers-color-scheme: dark)").matches}}var q=new P;export{q as default}; diff --git a/Resources/Public/JavaScript/dashboard.js b/Resources/Public/JavaScript/dashboard.js new file mode 100644 index 0000000..0d08c71 --- /dev/null +++ b/Resources/Public/JavaScript/dashboard.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as q,html as h,nothing as D}from"lit";import{state as E,query as H,customElement as M,property as W}from"lit/decorators.js";import{repeat as O}from"lit/directives/repeat.js";import{unsafeHTML as K}from"lit/directives/unsafe-html.js";import{styleMap as B}from"lit/directives/style-map.js";import{Task as G}from"@lit/task";import{fadeOut as Q,fadeIn as V,animate as J}from"@lit-labs/motion";import"@typo3/backend/element/icon-element.js";import p from"@typo3/core/ajax/ajax-request.js";import Y from"@typo3/backend/storage/client.js";import{delay as Z}from"@typo3/core/lit-helper.js";import b from"@typo3/backend/modal.js";import{SeverityEnum as x}from"@typo3/backend/enum/severity.js";import{AjaxResponse as _}from"@typo3/core/ajax/ajax-response.js";import{Categories as tt}from"@typo3/backend/new-record-wizard.js";import{topLevelModuleImport as j}from"@typo3/backend/utility/top-level-module-import.js";import{selector as et}from"@typo3/core/literals.js";import U from"@typo3/backend/utility/dom-helper.js";import w from"@typo3/backend/notification.js";import{SettingsEditorSubmitEvent as it}from"@typo3/backend/settings/editor.js";import r from"~labels/dashboard.messages";var v=function(u,t,e,s){var a=arguments.length,i=a<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,e):s,n;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")i=Reflect.decorate(u,t,e,s);else for(var d=u.length-1;d>=0;d--)(n=u[d])&&(i=(a<3?n(i):a>3?n(t,e,i):n(t,e))||i);return a>3&&i&&Object.defineProperty(t,e,i),i},f;(function(u){u.start="start",u.end="end",u.left="left",u.right="right",u.up="up",u.down="down"})(f||(f={}));const st="typo3:dashboard:widget:add";class S extends Event{static{this.eventName="typo3:dashboard:widget:content:rendered"}constructor(t){super(S.eventName,{bubbles:!0,composed:!0,cancelable:!1}),this.widget=t}}class $ extends Event{static{this.eventName="typo3:dashboard:widget:moveIntend"}constructor(t,e){super($.eventName,{bubbles:!0,composed:!0,cancelable:!1}),this.identifier=t,this.intend=e}}class A extends Event{static{this.eventName="typo3:dashboard:widget:remove"}constructor(t){super(A.eventName,{bubbles:!0,composed:!0,cancelable:!1}),this.identifier=t}}class T extends Event{static{this.eventName="typo3:dashboard:widget:refresh"}constructor(t){super(T.eventName,{bubbles:!0,composed:!0,cancelable:!1}),this.identifier=t}}class L extends Event{static{this.eventName="typo3:dashboard:dashboard:add"}constructor(t,e){super(L.eventName),this.preset=t,this.title=e}}class N extends Event{static{this.eventName="typo3:dashboard:dashboard:edit"}constructor(t,e){super(N.eventName),this.identifier=t,this.title=e}}class P extends Event{static{this.eventName="typo3:dashboard:dashboard:update"}constructor(t,e,s){super(P.eventName),this.identifier=t,this.widgets=e,this.widgetPositions=s}}class z extends Event{static{this.eventName="typo3:dashboard:dashboard:delete"}constructor(t){super(z.eventName),this.identifier=t}}function R(u){const t=new Set;for(let e=0;e{this.prefersReducedMotion=t.matches},this.addEventListener(T.eventName,t=>{t.preventDefault(),this.getGridItemByIdentifier(t.identifier).querySelector("typo3-dashboard-widget").refresh()}),this.addEventListener(A.eventName,t=>{t.preventDefault();const{identifier:e}=t;new p(TYPO3.settings.ajaxUrls.dashboard_widget_remove).post({dashboard:this.currentDashboard.identifier,identifier:e}).then(async s=>{const a=await s.resolve();if(a.status==="ok"){this.currentDashboard.widgets=this.currentDashboard.widgets.filter(i=>i.identifier!==e);for(const[i,n]of Object.entries(this.currentDashboard.widgetPositions)){const d=Number(i);this.currentDashboard.widgetPositions[d]=n.filter(c=>c.identifier!==e)}this.requestUpdate()}else w.error("",a.message)})}),this.addEventListener($.eventName,t=>{t.preventDefault();const{intend:e,identifier:s}=t,a=this.widgetPositionByIdentifier(s);switch(e){case f.up:a.y=Math.max(0,a.y-1);break;case f.down:a.y++;break;case f.left:a.x=Math.max(0,a.x-1);break;case f.right:a.x=Math.min(this.columns-a.width,a.x+1);break;case f.end:document.activeElement instanceof HTMLElement&&document.activeElement.closest("typo3-dashboard")===this&&(this.activeElementRef=document.activeElement),this.widgetPositionsSort(this.currentDashboard.widgetPositions[this.columns]),this.dispatchEvent(new P(this.currentDashboard.identifier,this.currentDashboard.widgets,this.currentDashboard.widgetPositions));return;default:return}this.widgetPositionChange(this.currentDashboard.widgetPositions[this.columns],a),this.updateComplete.then(()=>{const i=this.getGridItemByIdentifier(s);if(i){const n=e!==f.up;U.scrollIntoViewIfNeeded(i,n)}})}),this.addEventListener(L.eventName,t=>{t.preventDefault();const{preset:e,title:s}=t;new p(TYPO3.settings.ajaxUrls.dashboard_dashboard_add).post({preset:e,title:s}).then(async a=>{const i=await a.resolve();if(i.status==="ok"){const n=i.dashboard;this.dashboards.push(n);const d=this.getDashboardByIdentifier(n.identifier)||this.getDashboardFirst();this.selectDashboard(d),this.requestUpdate()}else w.error("",i.message)})}),this.addEventListener(N.eventName,t=>{t.preventDefault();const{identifier:e,title:s}=t;new p(TYPO3.settings.ajaxUrls.dashboard_dashboard_edit).post({identifier:e,title:s}).then(async a=>{const i=await a.resolve();if(i.status==="ok"){const n=this.dashboards.filter(l=>l.identifier===e)[0],d=this.dashboards.indexOf(n),c=i.dashboard;this.dashboards[d]=c,n.identifier===c.identifier&&this.selectDashboard(c),this.requestUpdate()}else w.error("",i.message)})}),this.addEventListener(P.eventName,t=>{t.preventDefault();const{identifier:e,widgets:s,widgetPositions:a}=t;new p(TYPO3.settings.ajaxUrls.dashboard_dashboard_update).post({identifier:e,widgets:s,widgetPositions:a}).then(async i=>{const n=await i.resolve();if(n.status==="ok"){const d=this.dashboards.filter(o=>o.identifier===e)[0],c=this.dashboards.indexOf(d),l=n.dashboard;this.dashboards[c]=l,d.identifier===l.identifier&&this.selectDashboard(l),this.requestUpdate()}else w.error("",n.message)})}),this.addEventListener(z.eventName,t=>{t.preventDefault();const{identifier:e}=t;new p(TYPO3.settings.ajaxUrls.dashboard_dashboard_delete).post({identifier:e}).then(async s=>{const a=await s.resolve();if(a.status==="ok"){this.dashboards=this.dashboards.filter(n=>n.identifier!==e);const i=this.getDashboardFirst();this.selectDashboard(i),this.requestUpdate()}else w.error("",a.message)})})}connectedCallback(){super.connectedCallback(),this.resizeObserver=new ResizeObserver(t=>{for(const e of t){const{width:s}=e.contentRect;s>950?this.columns=4:s>750?this.columns=2:this.columns=1}}),this.resizeObserver.observe(this),this.mql=window.matchMedia("(prefers-reduced-motion: reduce)"),this.mqListener(this.mql),this.mql.addEventListener("change",this.mqListener)}disconnectedCallback(){super.disconnectedCallback(),this.resizeObserver?.disconnect(),this.resizeObserver=null,this.mql?.removeEventListener("change",this.mqListener),this.mql=null}firstUpdated(){this.load()}updated(){this.activeElementRef&&(this.activeElementRef.focus(),this.activeElementRef=null)}createRenderRoot(){return this}render(){return this.loading?this.renderLoader():h`${this.renderHeader()}
${this.renderContent()}
${this.renderFooter()}`}async load(){this.loading=!0,this.dashboards=await this.fetchDashboards();const t=Y.get(this.clientStorageIdentifier),e=this.getDashboardByIdentifier(t)||this.getDashboardFirst();this.selectDashboard(e),this.loading=!1}async fetchData(t){try{return(await new p(t).get({cache:"no-cache"})).resolve()}catch(e){return console.error(e),[]}}async fetchPresets(){const t=await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_presets_get);return Object.values(t)}async fetchCategories(){const t=await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_categories_get);return tt.fromData(t)}async fetchDashboards(){return await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_dashboards_get)}getDashboardByIdentifier(t){return this.dashboards.find(e=>e.identifier===t)||null}getDashboardFirst(){return this.dashboards.length>0?this.dashboards[0]:null}async createDashboard(){const e=(await this.fetchPresets()).filter(a=>a.showInWizard),s=h`
${O(e,a=>a.identifier,(a,i)=>h`
`)}
`;b.advanced({type:b.types.default,title:r.get("dashboard.add"),size:b.sizes.medium,severity:x.notice,content:s,callback:a=>{a.addEventListener("typo3-modal-shown",()=>{a.querySelector("#dashboard-form-add-title")?.focus()}),a.querySelector("form").addEventListener("submit",i=>{i.preventDefault();const n=i.target,d=new FormData(n);this.dispatchEvent(new L(d.get("preset"),d.get("title"))),a.hideModal()})},buttons:[{text:r.get("dashboard.add.button.close"),btnClass:"btn-default",name:"cancel",trigger:(a,i)=>i.hideModal()},{text:r.get("dashboard.add.button.ok"),btnClass:"btn-primary",name:"save",trigger:(a,i)=>i.querySelector("form").requestSubmit()}]})}editDashboard(t){const e=h`
`;b.advanced({type:b.types.default,title:r.get("dashboard.configure"),size:b.sizes.small,severity:x.notice,content:e,callback:s=>{s.addEventListener("typo3-modal-shown",()=>{s.querySelector("#dashboard-form-edit-title")?.focus()}),s.querySelector("form").addEventListener("submit",a=>{a.preventDefault();const i=a.target,n=new FormData(i);this.dispatchEvent(new N(t.identifier,n.get("title"))),s.hideModal()})},buttons:[{text:r.get("dashboard.configure.button.close"),btnClass:"btn-default",name:"cancel",trigger:(s,a)=>a.hideModal()},{text:r.get("dashboard.configure.button.ok"),btnClass:"btn-primary",name:"save",trigger:(s,a)=>a.querySelector("form").requestSubmit()}]})}deleteDashboard(t){const e=b.confirm(r.get("dashboard.delete"),r.get("dashboard.delete.sure"),x.warning,[{text:r.get("dashboard.delete.cancel"),active:!0,btnClass:"btn-default",name:"cancel"},{text:r.get("dashboard.delete.ok"),btnClass:"btn-warning",name:"delete"}]);e.addEventListener("button.clicked",s=>{s.target.getAttribute("name")==="delete"&&this.dispatchEvent(new z(t.identifier)),e.hideModal()})}selectDashboard(t){t!==null&&Y.set(this.clientStorageIdentifier,t.identifier),this.currentDashboard=t}async addWidget(){j("@typo3/backend/new-record-wizard.js");const t=top.document.createElement("typo3-backend-new-record-wizard");t.storeName="dashboard-widgets",t.searchPlaceholder=r.get("widget.addToDashboard.searchLabel"),t.searchNothingFoundLabel=r.get("widget.addToDashboard.searchNotFound"),t.userNotAllowedLabel=r.get("widget.addToDashboard.userNotAllowed"),t.categories=await this.fetchCategories(),t.addEventListener(st,async e=>{const{identifier:s}=e.detail.item,i=await(await new p(TYPO3.settings.ajaxUrls.dashboard_widget_add).post({dashboard:this.currentDashboard.identifier,type:s})).resolve();if(i.status==="ok"){this.currentDashboard.widgets.push(i.widget),this.requestUpdate(),await this.updateComplete;const n=this.getGridItemByIdentifier(i.widget.identifier);n&&(U.scrollIntoViewIfNeeded(n,!0),window.setTimeout(()=>n.querySelector(".widget-actions > button:first-child")?.focus({preventScroll:!0,focusVisible:!1}),50))}else w.error("",i.message)}),b.advanced({type:b.types.default,title:r.get("widget.addToDashboard",[this.currentDashboard.title]),size:b.sizes.medium,severity:x.notice,content:t,callback:e=>{e.addEventListener("button.clicked",()=>{e.hideModal()})},buttons:[{text:r.get("widget.add.button.close"),btnClass:"btn-default",name:"cancel"}]})}renderLoader(){return h`
`}renderHeader(){const t=h``,e=this.currentDashboard!==null?h``:D,s=this.currentDashboard!==null?h``:D;return h`

${this.currentDashboard?.title}

${O(this.dashboards,a=>a.identifier,a=>h``)} ${t}
${e||s?h``:D}
`}renderContent(){if(this.currentDashboard){if(this.currentDashboard.widgets.length>0){this.initializeCurrentDashboard();const t={keyframeOptions:{duration:250,fill:"both"},in:V,out:Q,skipInitial:!0,disabled:this.prefersReducedMotion};return h`
${O(this.currentDashboard.widgetPositions[this.columns],e=>e.identifier,e=>h`
s.target.setAttribute("draggable","true")} @widgetRefresh=${()=>this.handleLegacyWidgetRefreshEvent(e)} ${J(t)}>
`)}
`}return h`

${r.get("dashboard.empty.content.title")}

${r.get("dashboard.empty.content.description")}

`}return D}renderFooter(){return this.currentDashboard===null?D:h`
`}getGridItemByIdentifier(t){return this.querySelector(et`.dashboard-item[data-widget-identifier="${t}"]`)}handleDragStart(t){const s=t.target.closest(".dashboard-item");if(s===null){t.preventDefault();return}if(document.elementFromPoint(t.clientX,t.clientY).closest(".widget-header")===null){t.preventDefault();return}const i=s.dataset.widgetIdentifier,n=this.widgetPositionByIdentifier(i),d=s.getBoundingClientRect(),c=s.querySelector("typo3-dashboard-widget");c.style.pointerEvents="none",this.dragInformation={identifier:i,itemElement:s,widgetElement:c,height:n.height,width:n.width,offsetY:t.clientY-d.top,offsetX:t.clientX-d.left,currentY:n.y,currentX:n.x,initialPositions:this.currentDashboard.widgetPositions[this.columns].map(o=>({...o}))};const l=new Image;l.src="data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",t.dataTransfer.setDragImage(l,0,0),t.dataTransfer.setData("text/plain",""),t.dataTransfer.effectAllowed="move",s.classList.add("dashboard-item-dragging"),this.positionDraggingElement(t),this.draggingContainer.appendChild(c)}positionDraggingElement(t){const e=(o,g,m)=>Math.min(m,Math.max(g,o)),{itemElement:s,widgetElement:a}=this.dragInformation,i=s.getBoundingClientRect(),n=this.querySelector(".dashboard-container").getBoundingClientRect(),d=20,c=e(t.clientX-this.dragInformation.offsetX,n.left-d,n.left+n.width-i.width+d),l=Math.max(n.top-d,t.clientY-this.dragInformation.offsetY);a.style.left=`${c}px`,a.style.top=`${l}px`,a.style.width=`${i.width}px`,a.style.height=`${i.height}px`}handleDragEnd(){if(this.dragInformation){const{itemElement:t,widgetElement:e}=this.dragInformation;t.classList.remove("dashboard-item-dragging"),t.appendChild(e),e.removeAttribute("style"),this.dragInformation=null,this.widgetPositionsSort(this.currentDashboard.widgetPositions[this.columns]),this.dispatchEvent(new P(this.currentDashboard.identifier,this.currentDashboard.widgets,this.currentDashboard.widgetPositions))}}handleDragOver(t){if(this.dragInformation){t.preventDefault(),t.dataTransfer.dropEffect="move",this.positionDraggingElement(t);const e=this.querySelector(".dashboard-grid"),s=e.getBoundingClientRect(),a=parseInt(getComputedStyle(e).gap,10),i=parseInt(getComputedStyle(e).gridAutoRows,10)+a,n=(s.width+a)/this.columns,d=Math.max(0,t.clientY-s.top-this.dragInformation.offsetY),c=Math.max(0,t.clientX-s.left-this.dragInformation.offsetX),l=Math.max(0,Math.round(d/i)),o=Math.max(0,Math.min(Math.round(c/n),this.columns-this.dragInformation.width));(this.dragInformation.currentY!==l||this.dragInformation.currentX!==o)&&(this.dragInformation.currentY=l,this.dragInformation.currentX=o,this.dragOverTimeout&&clearTimeout(this.dragOverTimeout),this.dragOverTimeout=window.setTimeout(()=>{if(this.dragInformation){const g=this.widgetPositionByIdentifier(this.dragInformation.identifier);g.y=this.dragInformation.currentY,g.x=this.dragInformation.currentX,this.widgetPositionChange(this.currentDashboard.widgetPositions[this.columns],g)}},100))}}handleLegacyWidgetRefreshEvent(t){this.dispatchEvent(new T(t.identifier))}initializeCurrentDashboard(){this.currentDashboard.widgetPositions=this.currentDashboard.widgetPositions??{};let t=this.currentDashboard.widgetPositions?.[this.columns]??[];const e={small:1,medium:2,large:4},s={small:1,medium:2,large:3};this.currentDashboard.widgets.forEach(a=>{if(t.find(i=>i.identifier===a.identifier)===void 0){const i=s[a.height]??1,n=e[a.width]??1,d={identifier:a.identifier,height:i,width:ne.identifier===t)??null}widgetPositionByIdentifier(t){return this.currentDashboard.widgetPositions[this.columns].find(e=>e.identifier===t)??null}widgetPositionCanPlace(t,e,s,a){return e<0||e>this.columns-t.width||s<0?!1:a.isDisjointFrom(R({...t,x:e,y:s}))}widgetPositionChange(t,e){let s=structuredClone(this.dragInformation?.initialPositions??t);const a=s.findIndex(n=>n.identifier===e.identifier);let i;if(a>-1){const[n]=s.splice(a,1);i={...n},n.y=e.y,n.x=e.x,s.unshift(n)}s=this.widgetPositionsArrange(s,this.dragInformation?.initialPositions??t,i),t.forEach(n=>{const d=s.find(c=>c.identifier===n.identifier);n.y=d.y,n.x=d.x}),this.widgetPositionsCollapseRows(t),this.requestUpdate()}widgetTryPlacementInNeighbourCells(t,e,s){const a=this.columns;for(let i=t.x;i>=Math.max(0,t.x-t.width);i--)if(this.widgetPositionCanPlace(t,i,t.y,e))return{...t,x:i};for(let i=t.y;i>=0;i--)if(this.widgetPositionCanPlace(t,t.x,i,e))return{...t,y:i};for(let i=t.x;i<=Math.min(a,t.x+t.width);i++)if(this.widgetPositionCanPlace(t,i,t.y,e))return{...t,x:i};for(let i=t.y;i<=t.y+(s?.height??3);i++)if(this.widgetPositionCanPlace(t,t.x,i,e))return{...t,y:i};return null}widgetPositionsArrange(t,e,s){let a=new Set;const i=o=>this.widgetPositionCanPlace(o,o.x,o.y,a)?{...o}:null,n=o=>e===void 0?null:this.widgetTryPlacementInNeighbourCells(o,e.reduce((g,m)=>g.union(R(m)),new Set).difference(R(s)).union(a),s),d=o=>this.widgetTryPlacementInNeighbourCells(o,a),c=o=>{const g=Math.max(0,o.y),m=Math.max(0,Math.min(this.columns-o.width,o.x)),F=Math.max(0,m),X=this.columns;for(let C=o.y;C(a=a.union(R(o)),o);return t.map(o=>l(i(o)??n(o)??d(o)??c(o)))}widgetPositionsCollapseRows(t){const e=new Set;t.forEach(i=>{for(let n=0;n{i.y=s[i.y]})}widgetPositionsSort(t){t.sort((e,s)=>e.y!==s.y?e.y-s.y:e.x-s.x)}};v([E()],y.prototype,"loading",void 0),v([E()],y.prototype,"dashboards",void 0),v([E()],y.prototype,"currentDashboard",void 0),v([E()],y.prototype,"columns",void 0),v([E()],y.prototype,"dragInformation",void 0),v([H(".dashboard-dragging-container")],y.prototype,"draggingContainer",void 0),y=v([M("typo3-dashboard")],y);let k=class extends q{constructor(){super(...arguments),this.moving=!1,this.triggerContentRenderedEvent=!1,this.fetchTask=new G(this,{args:()=>[this.identifier],task:async([t],{signal:e})=>{const s=TYPO3.settings.ajaxUrls.dashboard_widget_get,i=await(await new p(s).withQueryArguments({widget:t}).get({signal:e})).resolve();if(i.status!=="ok")throw new Error(i.message);return i.widget},onComplete:async()=>{this.triggerContentRenderedEvent=!0},onError:t=>{console.error(`Error while retrieving widget [${this.identifier}]: ${t instanceof _?`${t.response.status} ${t.response.statusText}`:t.message}`)}})}get widget(){return this.fetchTask.value??null}refresh(){this.handleRefresh()}createRenderRoot(){return this}updated(){if(this.triggerContentRenderedEvent){this.triggerContentRenderedEvent=!1;const{widget:t}=this;this.dispatchEvent(new S(t)),this.dispatchEvent(new CustomEvent("widgetContentRendered",{bubbles:!0,detail:this.widget.eventdata}))}}render(){const t=h`
`,e=c=>c?.label||"ERROR",s=c=>c?K(c.content):h`
${r.get("widget.error")}
`,a=()=>h``,i=(c=!1)=>h``,n=(c,l=!1)=>h`
${e(c)}
${c?.configurable?a():D} ${c?.refreshable?i(l):D}
o.target.closest(".dashboard-item").removeAttribute("draggable")} @pointerleave=${o=>o.target.closest(".dashboard-item").setAttribute("draggable","true")}>${s(c)}
`,d=this.fetchTask.render({initial:()=>D,error:()=>n(null),pending:()=>this.fetchTask.value?n(this.fetchTask.value,!0):Z(80,()=>t),complete:c=>n(c)});return h`
${d}
`}moveStart(){this.moving===!1&&(this.moving=!0,this.dispatchEvent(new $(this.widget.identifier,f.start)))}moveEnd(){this.moving===!0&&(this.moving=!1,this.dispatchEvent(new $(this.widget.identifier,f.end)))}handleMoveClick(){this.moving?this.moveEnd():this.moveStart()}handleMoveFocusOut(){this.moveEnd()}handleMoveKeyDown(t){if(!this.moving||!["ArrowDown","ArrowUp","ArrowLeft","ArrowRight","Home","End","Enter","Space","Escape","Tab"].includes(t.code)||t.altKey||t.ctrlKey)return;t.preventDefault(),t.stopPropagation();let s=f.end;switch(t.code){case"Escape":case"Enter":case"Space":this.moveEnd();return;case"ArrowUp":s=f.up;break;case"ArrowDown":s=f.down;break;case"ArrowLeft":s=f.left;break;case"ArrowRight":s=f.right;break;default:return}this.dispatchEvent(new $(this.widget.identifier,s))}async editSettings(){j("@typo3/backend/settings/editor.js");const t=`widget_settings_form_${this.identifier}`,s=await(await new p(TYPO3.settings.ajaxUrls.dashboard_widget_settings_get).withQueryArguments({widget:this.widget.identifier}).get({cache:"no-cache"})).resolve();if(s.status!=="ok")throw new Error(s.message);const a=h``;b.advanced({type:b.types.default,title:r.get("widget.settings"),size:b.sizes.default,severity:x.notice,content:a,callback:i=>{i.querySelector("typo3-backend-settings-editor").addEventListener(it.eventName,async d=>{const{originalEvent:c,formData:l}=d,{settings:o}=l;c.preventDefault();try{const m=await(await new p(TYPO3.settings.ajaxUrls.dashboard_widget_settings_update).post({widget:this.widget.identifier,settings:o})).resolve();m.status==="ok"?(this.handleRefresh(),i.hideModal()):m.status==="info"?w.info("",m.message):w.error("",m.message)}catch(g){if(g instanceof _)w.error("",g.response.status+" "+g.response.statusText,5),console.error(g);else if(g instanceof Error)w.error("",g.message),console.error(g);else throw g}})},buttons:[{text:r.get("widget.settings.button.close"),btnClass:"btn-default",name:"cancel",trigger:(i,n)=>n.hideModal()},{text:r.get("widget.settings.button.save"),btnClass:"btn-primary",name:"save",form:t}]})}handleRefresh(){this.fetchTask.run()}handleRemove(t){const e=b.confirm(r.get("widget.remove.confirm.title"),r.get("widget.remove.confirm.message"),x.warning,[{text:r.get("widget.remove.button.close"),active:!0,btnClass:"btn-default",name:"cancel"},{text:r.get("widget.remove.button.ok"),btnClass:"btn-warning",name:"delete"}]);e.addEventListener("button.clicked",a=>{a.target.getAttribute("name")==="delete"&&this.dispatchEvent(new A(this.identifier)),e.hideModal()});const s=t.currentTarget;e.addEventListener("typo3-modal-hide",()=>{s?.focus()})}};v([W({type:String,reflect:!0})],k.prototype,"identifier",void 0),v([E()],k.prototype,"moving",void 0),k=v([M("typo3-dashboard-widget")],k);export{y as Dashboard,k as DashboardWidget,S as DashboardWidgetContentRenderedEvent}; diff --git a/Resources/Public/JavaScript/widget/bookmarks-widget-element.js b/Resources/Public/JavaScript/widget/bookmarks-widget-element.js new file mode 100644 index 0000000..540fe22 --- /dev/null +++ b/Resources/Public/JavaScript/widget/bookmarks-widget-element.js @@ -0,0 +1,13 @@ +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ +import{LitElement as f,html as d,nothing as k}from"lit";import{property as m,state as c,customElement as y}from"lit/decorators.js";import{repeat as h}from"lit/directives/repeat.js";import g from"~labels/core.bookmarks";import u,{BookmarkStoreChangedEvent as b}from"@typo3/backend/bookmark/bookmark-store.js";import"@typo3/backend/bookmark/bookmark-manager.js";var l=function(r,t,e,s){var a=arguments.length,o=a<3?t:s===null?s=Object.getOwnPropertyDescriptor(t,e):s,i;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")o=Reflect.decorate(r,t,e,s);else for(var p=r.length-1;p>=0;p--)(i=r[p])&&(o=(a<3?i(o):a>3?i(t,e,o):i(t,e))||o);return a>3&&o&&Object.defineProperty(t,e,o),o};let n=class extends f{constructor(){super(...arguments),this.limit=0,this.groupId=null,this.groupedBookmarks=new Map,this.groups=[],this.handleStoreUpdate=()=>{this.syncFromStore()}}connectedCallback(){super.connectedCallback(),document.addEventListener(b,this.handleStoreUpdate),this.syncFromStore()}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener(b,this.handleStoreUpdate)}createRenderRoot(){return this}render(){return this.groupedBookmarks.size===0?d``:d`
${h(Array.from(this.groupedBookmarks.entries()),([t])=>t,([t,e])=>this.renderGroup(t,e,this.groupedBookmarks.size>1))}
`}async syncFromStore(){this.groupedBookmarks=await u.getGroupedBookmarks({groupId:this.groupId??void 0,limit:this.limit>0?this.limit:void 0,accessibleOnly:!0}),this.groups=await u.getGroups()}renderGroup(t,e,s){const o=this.groups.find(i=>i.id===t)?.label||g.get("notGrouped");return d`${s&&this.groupId===null?d``:k}${h(e,i=>i.id,i=>this.renderBookmarkRow(i))}
${o}
`}renderBookmarkRow(t){return d` this.handleNavigate(e,t)}> ${t.title} `}handleNavigate(t,e){t.preventDefault(),u.navigate(e)}};l([m({type:Number})],n.prototype,"limit",void 0),l([m({attribute:"group",converter:{fromAttribute:r=>{if(r===null||r==="")return null;const t=Number(r);return Number.isNaN(t)?r:t},toAttribute:r=>r===null?null:String(r)}})],n.prototype,"groupId",void 0),l([c()],n.prototype,"groupedBookmarks",void 0),l([c()],n.prototype,"groups",void 0),n=l([y("typo3-dashboard-bookmarks-widget")],n);export{n as BookmarksWidgetElement}; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..03eb335 --- /dev/null +++ b/composer.json @@ -0,0 +1,61 @@ +{ + "name": "typo3/cms-dashboard", + "type": "typo3-cms-framework", + "description": "TYPO3 CMS Dashboard - TYPO3 backend module used to configure and create backend widgets.", + "homepage": "https://typo3.community/", + "funding": [ + { + "type": "membership", + "url": "https://typo3.org/membership" + } + ], + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "TYPO3 Core Team", + "email": "typo3cms@typo3.org", + "role": "Developer" + } + ], + "support": { + "issues": "https://forge.typo3.org/issues/", + "forum": "https://talk.typo3.org/", + "source": "https://github.com/TYPO3/typo3/", + "docs": "https://docs.typo3.org/c/typo3/cms-dashboard/main/en-us/", + "rss": "https://news.typo3.com/rss/", + "chat": "https://typo3.community/meet/slack/", + "security": "https://typo3.org/security/" + }, + "config": { + "sort-packages": true + }, + "require": { + "typo3/cms-core": "15.0.*@dev", + "typo3/cms-frontend": "15.0.*@dev", + "typo3/cms-backend": "15.0.*@dev", + "typo3/cms-fluid": "15.0.*@dev", + "typo3/cms-extbase": "15.0.*@dev" + }, + "conflict": { + "typo3/cms": "*" + }, + "extra": { + "branch-alias": { + "dev-main": "15.0.x-dev" + }, + "typo3/cms": { + "extension-key": "dashboard", + "Package": { + "serviceProvider": "TYPO3\\CMS\\Dashboard\\ServiceProvider", + "partOfFactoryDefault": true + } + } + }, + "autoload": { + "psr-4": { + "TYPO3\\CMS\\Dashboard\\": "Classes/" + } + } +} diff --git a/ext_localconf.php b/ext_localconf.php new file mode 100644 index 0000000..710b45b --- /dev/null +++ b/ext_localconf.php @@ -0,0 +1,22 @@ + VariableFrontend::class, + 'backend' => FileBackend::class, + 'options' => [ + 'defaultLifetime' => 900, + ], + ]; +} + +// Fill the "owner" field of a dashboard with the user who created it +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = DashboardCreationEnricher::class; diff --git a/ext_tables.sql b/ext_tables.sql new file mode 100644 index 0000000..a27dd09 --- /dev/null +++ b/ext_tables.sql @@ -0,0 +1,7 @@ +CREATE TABLE be_dashboards ( + # type=passthrough needs manual configuration + cruser_id int(11) unsigned DEFAULT 0 NOT NULL, + # No TCA column + widgets text, + KEY identifier (identifier) +);