Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordizzy <diosmosis@users.noreply.github.com>2021-10-15 09:08:14 +0300
committerGitHub <noreply@github.com>2021-10-15 09:08:14 +0300
commit2f80606b1db3caaeb4195ff7cd0bf949b2154968 (patch)
tree3ebe7333835309b135395a3b31496f9bee93c9ed /plugins/CoreHome/vue/src
parent71c98f36ec238b6d688065963f196c387850da53 (diff)
[Vue] utility function for creating angularjs adapters (#18146)4.6.0-b1
* incomplete conversion * get ajax helper migration to work * delete old periods.spec.js * remove global-ajax-queue.js file * migrate piwik service and test (w/ hacks to get it the test to work) * rebuild and remove old files + get tests to pass * unfinished commit * return jqxhr object so promise api can be used * move hasBlockedContent and deprecate piwikApi service * remove alert files * convert piwikHelper.spec.js * in new vue code, use "Matomo" everywhere possible instead of "piwik" and rebuild vue files * add another needed export line in command * include polyfills after vue so we can add to vue engine * Add HTML sanitizer for use w/ migrating ng-bind-html uses. * fix broken merge, rebuild js, fix issue in build command * add sanitize to other components for consistency (will be replaced by utility function eventually) * migrate matomo-dialog, fix issue where vue:build --watch did not correctly watch all plugin files, fix path issue in webpack externals, add vue matomo dialog use example to ExampleVue * update expected screenshot * create initial createAngularJsAdapter generic function and use for AcitivityIndicator * fix webpack chunk loading issue that occurs only on production (since the chunk is not stored in the same directory as the merged asset JS) * use adapter utility for Alert * use adapter function for matomo-dialog (even though the mapping becomes more complex) * fix unit tests
Diffstat (limited to 'plugins/CoreHome/vue/src')
-rw-r--r--plugins/CoreHome/vue/src/ActivityIndicator/ActivityIndicator.adapter.ts58
-rw-r--r--plugins/CoreHome/vue/src/Alert/Alert.adapter.ts63
-rw-r--r--plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts64
-rw-r--r--plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.vue58
-rw-r--r--plugins/CoreHome/vue/src/createAngularJsAdapter.ts180
-rw-r--r--plugins/CoreHome/vue/src/index.ts3
6 files changed, 332 insertions, 94 deletions
diff --git a/plugins/CoreHome/vue/src/ActivityIndicator/ActivityIndicator.adapter.ts b/plugins/CoreHome/vue/src/ActivityIndicator/ActivityIndicator.adapter.ts
index 0ca0ac8241..84cb965fc6 100644
--- a/plugins/CoreHome/vue/src/ActivityIndicator/ActivityIndicator.adapter.ts
+++ b/plugins/CoreHome/vue/src/ActivityIndicator/ActivityIndicator.adapter.ts
@@ -5,51 +5,23 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
-import { createApp } from 'vue';
import ActivityIndicator from './ActivityIndicator.vue';
import translate from '../translate';
+import createAngularJsAdapter from '../createAngularJsAdapter';
-interface ActivityIndicatorAdapterScope extends ng.IScope {
- loading: boolean;
- loadingMessage: string;
-}
-
-export default function activityIndicatorAdapter(): ng.IDirective {
- return {
- restrict: 'A',
- scope: {
- loading: '<',
- loadingMessage: '<',
+export default createAngularJsAdapter({
+ component: ActivityIndicator,
+ scope: {
+ loading: {
+ vue: 'loading',
+ angularJsBind: '<',
},
- template: '',
- link: function activityIndicatorAdapterLink(
- scope: ActivityIndicatorAdapterScope,
- element: ng.IAugmentedJQuery,
- ) {
- const app = createApp({
- template: '<activity-indicator :loading="loading" :loadingMessage="loadingMessage"/>',
- data() {
- return {
- loading: scope.loading,
- loadingMessage: scope.loadingMessage,
- };
- },
- });
- app.config.globalProperties.$sanitize = window.vueSanitize;
- app.component('activity-indicator', ActivityIndicator);
- const vm = app.mount(element[0]);
-
- scope.$watch('loading', (newValue: boolean) => {
- vm.loading = newValue;
- });
-
- scope.$watch('loadingMessage', (newValue: string) => {
- vm.loadingMessage = newValue || translate('General_LoadingData');
- });
+ loadingMessage: {
+ vue: 'loadingMessage',
+ angularJsBind: '<',
+ default: () => translate('General_LoadingData'),
},
- };
-}
-
-activityIndicatorAdapter.$inject = [];
-
-angular.module('piwikApp').directive('piwikActivityIndicator', activityIndicatorAdapter);
+ },
+ $inject: [],
+ directiveName: 'piwikActivityIndicator',
+});
diff --git a/plugins/CoreHome/vue/src/Alert/Alert.adapter.ts b/plugins/CoreHome/vue/src/Alert/Alert.adapter.ts
index 5cecb97a81..c100116645 100644
--- a/plugins/CoreHome/vue/src/Alert/Alert.adapter.ts
+++ b/plugins/CoreHome/vue/src/Alert/Alert.adapter.ts
@@ -5,56 +5,17 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
-import { createApp, ref } from 'vue';
import Alert from './Alert.vue';
-
-interface AlertAdapterScope extends ng.IScope {
- severity: string;
-}
-
-export default function alertAdapter(): ng.IDirective {
- return {
- restrict: 'A',
- transclude: true,
- scope: {
- severity: '@piwikAlert',
- },
- template: '<div ng-transclude/>',
- compile: function alertAdapterCompile() {
- return {
- post: function alertAdapterPostLink(
- scope: AlertAdapterScope,
- element: ng.IAugmentedJQuery,
- ) {
- const clone = element.find('[ng-transclude]');
-
- const app = createApp({
- template: '<alert :severity="severity"><div ref="transcludeTarget"/></alert>',
- data() {
- return { severity: scope.severity };
- },
- setup() {
- const transcludeTarget = ref(null);
- return {
- transcludeTarget,
- };
- },
- });
- app.config.globalProperties.$sanitize = window.vueSanitize;
- app.component('alert', Alert);
- const vm = app.mount(element[0]);
-
- scope.$watch('severity', (newValue: string) => {
- vm.severity = newValue;
- });
-
- $(vm.transcludeTarget).append(clone);
- },
- };
+import createAngularJsAdapter from '../createAngularJsAdapter';
+
+export default createAngularJsAdapter({
+ component: Alert,
+ scope: {
+ severity: {
+ vue: 'severity',
+ angularJsBind: '@piwikAlert',
},
- };
-}
-
-alertAdapter.$inject = [];
-
-angular.module('piwikApp').directive('piwikAlert', alertAdapter);
+ },
+ directiveName: 'piwikAlert',
+ transclude: true,
+});
diff --git a/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts b/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts
new file mode 100644
index 0000000000..0b318173e7
--- /dev/null
+++ b/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts
@@ -0,0 +1,64 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+import { IParseService } from 'angular';
+import { ComponentPublicInstance } from 'vue';
+import MatomoDialog from './MatomoDialog.vue';
+import createAngularJsAdapter from '../createAngularJsAdapter';
+
+export default createAngularJsAdapter<[IParseService]>({
+ component: MatomoDialog,
+ scope: {
+ show: {
+ vue: 'show',
+ default: false,
+ },
+ element: {
+ vue: 'element',
+ default: (scope, element) => element[0],
+ },
+ },
+ events: {
+ yes: (scope, element, attrs) => {
+ if (attrs.yes) {
+ scope.$eval(attrs.yes);
+ setTimeout(() => { scope.$apply(); }, 0);
+ }
+ },
+ no: (scope, element, attrs) => {
+ if (attrs.no) {
+ scope.$eval(attrs.no);
+ setTimeout(() => { scope.$apply(); }, 0);
+ }
+ },
+ close: (scope, element, attrs) => {
+ if (attrs.close) {
+ scope.$eval(attrs.close);
+ setTimeout(() => { scope.$apply(); }, 0);
+ }
+ },
+ closeEnd: (scope, element, attrs, $parse: IParseService) => {
+ setTimeout(() => {
+ scope.$apply($parse(attrs.piwikDialog).assign(scope, false));
+ }, 0);
+ },
+ },
+ $inject: ['$parse'],
+ directiveName: 'piwikDialog',
+ transclude: true,
+ mountPointFactory: (scope, element) => {
+ const vueRootPlaceholder = $('<div class="vue-placeholder"/>');
+ vueRootPlaceholder.appendTo(element);
+ return vueRootPlaceholder[0];
+ },
+ postCreate: (vm: ComponentPublicInstance, scope, element, attrs) => {
+ scope.$watch(attrs.piwikDialog, (newValue: boolean) => {
+ vm.show = newValue || false;
+ });
+ },
+ noScope: true,
+});
diff --git a/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.vue b/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.vue
new file mode 100644
index 0000000000..5a46841cbf
--- /dev/null
+++ b/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.vue
@@ -0,0 +1,58 @@
+<!--
+ Matomo - free/libre analytics platform
+
+ @link https://matomo.org
+ @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+-->
+<template>
+ <slot></slot>
+</template>
+<script lang="ts">
+import { defineComponent } from 'vue';
+import Matomo from '../Matomo/Matomo';
+
+export default defineComponent({
+ props: {
+ /**
+ * Whether the modal is displayed or not;
+ */
+ show: {
+ type: Boolean,
+ required: true,
+ },
+
+ /**
+ * Only here for backwards compatibility w/ AngularJS. If supplied, we use this
+ * element to launch the modal instead of the element in the slot. This should not
+ * be used for new Vue code.
+ *
+ * @deprecated
+ */
+ element: {
+ type: HTMLElement,
+ required: false,
+ },
+ },
+ emits: ['yes', 'no', 'closeEnd', 'close'],
+ activated() {
+ const slotElement = this.element || this.$slots.default()[0].el;
+ slotElement.style.display = 'none';
+ },
+ watch: {
+ show(newValue, oldValue) {
+ if (newValue) {
+ const slotElement = this.element || this.$slots.default()[0].el;
+ Matomo.helper.modalConfirm(slotElement, {
+ yes: () => { this.$emit('yes'); },
+ no: () => { this.$emit('no'); },
+ }, {
+ onCloseEnd: () => { this.$emit('closeEnd'); },
+ });
+ } else if (newValue === false && oldValue === true) {
+ // the user closed the dialog, e.g. by pressing Esc or clicking away from it
+ this.$emit('close');
+ }
+ },
+ },
+});
+</script>
diff --git a/plugins/CoreHome/vue/src/createAngularJsAdapter.ts b/plugins/CoreHome/vue/src/createAngularJsAdapter.ts
new file mode 100644
index 0000000000..9d7ba86cea
--- /dev/null
+++ b/plugins/CoreHome/vue/src/createAngularJsAdapter.ts
@@ -0,0 +1,180 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+import {
+ createApp,
+ defineComponent,
+ ref,
+ ComponentPublicInstance,
+} from 'vue';
+
+interface SingleScopeVarInfo {
+ vue: string;
+ default?: any; // eslint-disable-line
+ angularJsBind?: string;
+}
+
+type ScopeMapping = { [scopeVarName: string]: SingleScopeVarInfo };
+
+type AdapterFunction<InjectTypes, R = void> = (
+ scope: ng.IScope,
+ element: ng.IAugmentedJQuery,
+ attrs: ng.IAttributes,
+ ...injected: InjectTypes,
+) => R;
+
+type PostCreateFunction<InjectTypes, R = void> = (
+ vm: ComponentPublicInstance,
+ scope: ng.IScope,
+ element: ng.IAugmentedJQuery,
+ attrs: ng.IAttributes,
+ ...injected: InjectTypes,
+) => R;
+
+type EventMapping<InjectTypes> = { [vueEventName: string]: AdapterFunction<InjectTypes> };
+
+type ComponentType = ReturnType<typeof defineComponent>;
+
+export default function createAngularJsAdapter<InjectTypes = []>(options: {
+ component: ComponentType,
+ scope?: ScopeMapping,
+ directiveName: string,
+ events?: EventMapping<InjectTypes>,
+ $inject?: string[],
+ transclude?: boolean,
+ mountPointFactory?: AdapterFunction<InjectTypes, HTMLElement>,
+ postCreate?: PostCreateFunction<InjectTypes>,
+ noScope?: boolean,
+}): ng.IDirectiveFactory {
+ const {
+ component,
+ scope = {},
+ events = {},
+ $inject,
+ directiveName,
+ transclude,
+ mountPointFactory,
+ postCreate,
+ noScope,
+ } = options;
+
+ const angularJsScope = {};
+ Object.entries(scope).forEach(([scopeVarName, info]) => {
+ if (info.angularJsBind) {
+ angularJsScope[scopeVarName] = info.angularJsBind;
+ }
+ });
+
+ function angularJsAdapter(...injectedServices: InjectTypes) {
+ const adapter: ng.IDirective = {
+ restrict: 'A',
+ scope: noScope ? undefined : angularJsScope,
+ compile: function angularJsAdapterCompile() {
+ return {
+ post: function angularJsAdapterLink(
+ ngScope: ng.IScope,
+ ngElement: ng.IAugmentedJQuery,
+ ngAttrs: ng.IAttributes,
+ ) {
+ const clone = ngElement.find('[ng-transclude]');
+
+ let rootVueTemplate = '<root-component';
+ Object.entries(scope).forEach(([, info]) => {
+ rootVueTemplate += ` :${info.vue}="${info.vue}"`;
+ });
+ Object.entries(events).forEach((info) => {
+ const [eventName] = info;
+ rootVueTemplate += ` @${eventName}="onEventHandler('${eventName}')"`;
+ });
+ rootVueTemplate += '>';
+ if (transclude) {
+ rootVueTemplate += '<div ref="transcludeTarget"/>';
+ }
+ rootVueTemplate += '</root-component>';
+ const app = createApp({
+ template: rootVueTemplate,
+ data() {
+ const initialData = {};
+ Object.entries(scope).forEach(([scopeVarName, info]) => {
+ let value = ngScope[scopeVarName];
+ if (typeof value === 'undefined' && typeof info.default !== 'undefined') {
+ value = info.default instanceof Function
+ ? info.default(ngScope, ngElement, ngAttrs, ...injectedServices)
+ : info.default;
+ }
+ initialData[info.vue] = value;
+ });
+ return initialData;
+ },
+ setup() {
+ if (transclude) {
+ const transcludeTarget = ref(null);
+ return {
+ transcludeTarget,
+ };
+ }
+
+ return undefined;
+ },
+ methods: {
+ onEventHandler(name: string) {
+ if (events[name]) {
+ events[name](ngScope, ngElement, ngAttrs, ...injectedServices);
+ }
+ },
+ },
+ });
+ app.config.globalProperties.$sanitize = window.vueSanitize;
+ app.component('root-component', component);
+
+ const mountPoint = mountPointFactory
+ ? mountPointFactory(ngScope, ngElement, ngAttrs, ...injectedServices)
+ : ngElement[0];
+ const vm = app.mount(mountPoint);
+
+ Object.entries(scope).forEach(([scopeVarName, info]) => {
+ if (!info.angularJsBind) {
+ return;
+ }
+
+ ngScope.$watch(scopeVarName, (newValue: any) => { // eslint-disable-line
+ if (typeof info.default !== 'undefined' && typeof newValue === 'undefined') {
+ vm[scopeVarName] = info.default instanceof Function
+ ? info.default(ngScope, ngElement, ngAttrs, ...injectedServices)
+ : info.default;
+ } else {
+ vm[scopeVarName] = newValue;
+ }
+ });
+ });
+
+ if (transclude) {
+ $(vm.transcludeTarget).append(clone);
+ }
+
+ if (postCreate) {
+ postCreate(vm, ngScope, ngElement, ngAttrs, ...injectedServices);
+ }
+ },
+ };
+ },
+ };
+
+ if (transclude) {
+ adapter.transclude = true;
+ adapter.template = '<div ng-transclude/>';
+ }
+
+ return adapter;
+ }
+
+ angularJsAdapter.$inject = $inject || [];
+
+ angular.module('piwikApp').directive(directiveName, angularJsAdapter);
+
+ return angularJsAdapter;
+}
diff --git a/plugins/CoreHome/vue/src/index.ts b/plugins/CoreHome/vue/src/index.ts
index d0b4b8a8fb..388204a6bd 100644
--- a/plugins/CoreHome/vue/src/index.ts
+++ b/plugins/CoreHome/vue/src/index.ts
@@ -17,7 +17,9 @@ import './Periods/Periods.adapter';
import './AjaxHelper/AjaxHelper.adapter';
import './PiwikUrl/PiwikUrl.adapter';
import './Piwik/Piwik.adapter';
+import './MatomoDialog/MatomoDialog.adapter';
+export { default as createAngularJsAdapter } from './createAngularJsAdapter';
export { default as activityIndicatorAdapter } from './ActivityIndicator/ActivityIndicator.adapter';
export { default as ActivityIndicator } from './ActivityIndicator/ActivityIndicator.vue';
export { default as translate } from './translate';
@@ -26,3 +28,4 @@ export { default as AjaxHelper } from './AjaxHelper/AjaxHelper';
export { default as MatomoUrl } from './MatomoUrl/MatomoUrl';
export { default as Matomo } from './Matomo/Matomo';
export * from './Periods';
+export { default as MatomoDialog } from './MatomoDialog/MatomoDialog.vue';