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:
Diffstat (limited to 'plugins/CoreHome/vue/src/SiteSelector')
-rw-r--r--plugins/CoreHome/vue/src/SiteSelector/AllSitesLink.vue36
-rw-r--r--plugins/CoreHome/vue/src/SiteSelector/SiteSelector.adapter.ts86
-rw-r--r--plugins/CoreHome/vue/src/SiteSelector/SiteSelector.less185
-rw-r--r--plugins/CoreHome/vue/src/SiteSelector/SiteSelector.vue370
-rw-r--r--plugins/CoreHome/vue/src/SiteSelector/SitesStore.ts131
5 files changed, 808 insertions, 0 deletions
diff --git a/plugins/CoreHome/vue/src/SiteSelector/AllSitesLink.vue b/plugins/CoreHome/vue/src/SiteSelector/AllSitesLink.vue
new file mode 100644
index 0000000000..548a697403
--- /dev/null
+++ b/plugins/CoreHome/vue/src/SiteSelector/AllSitesLink.vue
@@ -0,0 +1,36 @@
+<!--
+ Matomo - free/libre analytics platform
+ @link https://matomo.org
+ @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+-->
+
+<template>
+ <div
+ @click="this.onClick($event)"
+ class="custom_select_all"
+ >
+ <a
+ @click="$event.preventDefault()"
+ v-html="$sanitize(allSitesText)"
+ tabindex="4"
+ :href="href"
+ />
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+ props: {
+ href: String,
+ allSitesText: String,
+ },
+ emits: ['click'],
+ methods: {
+ onClick(event: MouseEvent) {
+ this.$emit('click', event);
+ },
+ },
+});
+</script>
diff --git a/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.adapter.ts b/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.adapter.ts
new file mode 100644
index 0000000000..b3438d2bb7
--- /dev/null
+++ b/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.adapter.ts
@@ -0,0 +1,86 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+import { INgModelController, ITimeoutService } from 'angular';
+import createAngularJsAdapter from '../createAngularJsAdapter';
+import SiteSelector from './SiteSelector.vue';
+import Matomo from '../Matomo/Matomo';
+
+export default createAngularJsAdapter<[ITimeoutService]>({
+ component: SiteSelector,
+ require: '?ngModel',
+ scope: {
+ showSelectedSite: {
+ angularJsBind: '=',
+ },
+ showAllSitesItem: {
+ angularJsBind: '=',
+ },
+ switchSiteOnSelect: {
+ angularJsBind: '=',
+ },
+ onlySitesWithAdminAccess: {
+ angularJsBind: '=',
+ },
+ name: {
+ angularJsBind: '@',
+ },
+ allSitesText: {
+ angularJsBind: '@',
+ },
+ allSitesLocation: {
+ angularJsBind: '@',
+ },
+ placeholder: {
+ angularJsBind: '@',
+ },
+ modelValue: {},
+ },
+ $inject: ['$timeout'],
+ directiveName: 'piwikSiteselector',
+ events: {
+ 'update:modelValue': (newValue, vm, scope, element, attrs, ngModel) => {
+ if ((newValue && !vm.modelValue)
+ || (!newValue && vm.modelValue)
+ || newValue.id !== vm.modelValue.id
+ ) {
+ element.attr('siteid', newValue.id);
+ element.trigger('change', newValue);
+
+ if (ngModel) {
+ ngModel.$setViewValue(newValue);
+ }
+ }
+ },
+ blur(event, vm, scope) {
+ setTimeout(() => scope.$apply());
+ },
+ },
+ postCreate(vm, scope, element, attrs, controller, $timeout: ITimeoutService) {
+ const ngModel = controller as INgModelController;
+
+ // setup ng-model mapping
+ if (ngModel) {
+ ngModel.$setViewValue(vm.modelValue);
+
+ ngModel.$render = () => {
+ if (angular.isString(ngModel.$viewValue)) {
+ vm.modelValue = JSON.parse(ngModel.$viewValue);
+ } else {
+ vm.modelValue = ngModel.$viewValue;
+ }
+ };
+ }
+
+ $timeout(() => {
+ if (attrs.siteid && attrs.sitename) {
+ vm.modelValue = { id: attrs.siteid, name: Matomo.helper.htmlDecode(attrs.sitename) };
+ ngModel.$setViewValue({ ...vm.modelValue });
+ }
+ });
+ },
+});
diff --git a/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.less b/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.less
new file mode 100644
index 0000000000..48700196c2
--- /dev/null
+++ b/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.less
@@ -0,0 +1,185 @@
+.autocompleteMatched {
+ color: #5256BE;
+ font-weight: bold;
+}
+.siteSelector {
+ a.title {
+ .icon.collapsed.iconHidden {
+ visibility: visible;
+ }
+
+ span.placeholder {
+ color: #9e9e9e;
+ font-style: italic;
+ }
+ }
+ .dropdown {
+ min-width: 210px;
+ }
+
+ .ui-widget.ui-widget-content {
+ border: none;
+ }
+}
+
+#content {
+ .sites_autocomplete {
+ position: static !important;
+ height: 36px;
+ z-index: 99;
+ vertical-align: middle;
+
+ > .siteSelector {
+ position: absolute;
+ z-index: 998;
+ }
+
+ a.title {
+ text-decoration: none;
+ }
+ }
+}
+
+.siteSelector.expanded {
+ .loading {
+ background: url(plugins/Morpheus/images/loading-blue.gif) no-repeat 16% 11px;
+ }
+}
+
+.siteSelector a.title,
+.siteSelector .custom_select_ul_list li a,
+.siteSelector .custom_select_all a,
+.siteSelector .custom_select_main_link > span {
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0;
+ color: @theme-color-text;
+ text-transform: uppercase;
+ width: 100%;
+}
+
+.siteSelector.piwikSelector a.title {
+ padding: 10px 15px 11px 13px;
+
+ /*.icon:not(.icon-fixed) {
+ margin-right: -11px;
+ }*/
+
+ > span {
+ max-width: 161px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ span {
+ vertical-align: top;
+ }
+}
+
+.siteSelector .custom_select_ul_list,
+.siteSelector ul.ui-autocomplete {
+ position: relative;
+ list-style: none;
+ line-height: 18px;
+ padding: 0 0 15px 0;
+ box-shadow: none !important;
+}
+
+.siteSelector .custom_select_ul_list {
+ padding: 0 0 5px 0;
+}
+
+.siteSelector .dropdown {
+ padding-top: 0;
+}
+
+.siteSelector .custom_select_ul_list li a,
+.siteSelector .custom_select_all a {
+ line-height: 18px;
+ height: auto;
+ display: block;
+ text-decoration: none;
+ padding-left: 5px;
+}
+
+.siteSelector .custom_select_ul_list li a:hover,
+.siteSelector .custom_select_all a:hover {
+ background: @color-silver-l95;
+}
+
+.siteSelector .custom_select_all a {
+ text-decoration: none;
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.siteSelector .custom_select_search {
+ margin: 0 0 8px 0;
+ height: 33px;
+ display: block;
+ white-space: nowrap;
+ position: relative;
+
+ .inp {
+ vertical-align: top;
+ width: 100%;
+ padding: 7px 6px !important;
+ border: 1px solid #d0d0d0 !important;
+ background: transparent !important;
+ font-size: 11px !important;
+ color: #454545 !important;
+ }
+ .reset {
+ position: absolute;
+ top: 8px;
+ right: 4px;
+ cursor: pointer;
+ }
+}
+
+.siteSelector {
+ width: auto;
+}
+
+.sites_selector_container>.siteSelector {
+ padding-left: 12px;
+}
+
+.custom_selector_container .ui-menu-item,
+.custom_selector_container .ui-menu-item a {
+ float:none;position:static
+}
+
+.custom_select_block_show {
+ height: auto;
+ overflow: visible;
+ max-width:inherit;
+}
+
+.sites_selector_container {
+ padding-top: 5px;
+}
+
+.siteSelect a {
+ white-space: normal;
+ text-align: left;
+}
+
+.siteSelector.disabled {
+ a.title {
+ cursor: default !important;
+
+ .icon {
+ display: none !important;
+ }
+ }
+
+ &.borderedControl {
+ &:hover {
+ background-color: @theme-color-background-base!important;
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.vue b/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.vue
new file mode 100644
index 0000000000..52884f6815
--- /dev/null
+++ b/plugins/CoreHome/vue/src/SiteSelector/SiteSelector.vue
@@ -0,0 +1,370 @@
+<!--
+ Matomo - free/libre analytics platform
+ @link https://matomo.org
+ @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+-->
+
+<template>
+ <div
+ class="siteSelector piwikSelector borderedControl"
+ :class="{'expanded': showSitesList, 'disabled': !hasMultipleSites}"
+ v-focus-anywhere-but-here="{ blur: onBlur }"
+ >
+ <input
+ v-if="name"
+ type="hidden"
+ :value="selectedSite?.id"
+ :name="name"
+ />
+ <a
+ ref="selectorLink"
+ @click="onClickSelector"
+ @keydown="onPressEnter($event)"
+ href="javascript:void(0)"
+ :class="{'loading': isLoading}"
+ class="title"
+ tabindex="4"
+ :title="selectorLinkTitle"
+ >
+ <span
+ class="icon icon-arrow-bottom"
+ :class="{'iconHidden': isLoading, 'collapsed': !showSitesList}"
+ />
+ <span>
+ <span
+ v-text="selectedSite?.name || firstSiteName"
+ v-if="selectedSite?.name || !placeholder"
+ />
+ <span
+ v-if="!selectedSite?.name && placeholder"
+ class="placeholder"
+ >{{ placeholder }}</span>
+ </span>
+ </a>
+ <div
+ v-show="showSitesList"
+ class="dropdown"
+ >
+ <div
+ class="custom_select_search"
+ v-show="autocompleteMinSites <= sites.length || searchTerm"
+ >
+ <input
+ type="text"
+ @click="searchTerm = '';loadInitialSites()"
+ v-model="searchTerm"
+ @keydown="onSearchInputKeydown()"
+ tabindex="4"
+ class="websiteSearch inp browser-default"
+ v-focus-if:[shouldFocusOnSearch]="{}"
+ :placeholder="translate('General_Search')"
+ />
+ <img
+ title="Clear"
+ v-show="searchTerm"
+ @click="searchTerm = '';loadInitialSites()"
+ class="reset"
+ src="plugins/CoreHome/images/reset_search.png"
+ />
+ </div>
+ <div v-if="allSitesLocation === 'top' && showAllSitesItem">
+ <AllSitesLink
+ :href="urlAllSites"
+ :all-sites-text="allSitesText"
+ @click="onAllSitesClick($event)"
+ />
+ </div>
+ <div class="custom_select_container">
+ <ul
+ class="custom_select_ul_list"
+ @click="showSitesList = false"
+ >
+ <li
+ @click="switchSite(site, $event)"
+ v-show="!(!showSelectedSite && activeSiteId === site.idsite)"
+ v-for="site in sites"
+ :key="site.idsite"
+ >
+ <a
+ @click="$event.preventDefault()"
+ v-html="$sanitize(getMatchedSiteName(site.name))"
+ tabindex="4"
+ :href="getUrlForSiteId(site.idsite)"
+ :title="site.name"
+ />
+ </li>
+ </ul>
+ <ul
+ v-show="!sites.length && searchTerm"
+ class="ui-autocomplete ui-front ui-menu ui-widget ui-widget-content ui-corner-all
+ siteSelect"
+ >
+ <li class="ui-menu-item">
+ <a
+ class="ui-corner-all"
+ tabindex="-1"
+ >
+ {{ translate('SitesManager_NotFound') + ' ' + searchTerm }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ <div v-if="allSitesLocation === 'bottom' && showAllSitesItem">
+ <AllSitesLink
+ :href="urlAllSites"
+ :all-sites-text="allSitesText"
+ @click="onAllSitesClick($event)"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FocusAnywhereButHere from '../FocusAnywhereButHere/FocusAnywhereButHere';
+import FocusIf from '../FocusIf/FocusIf';
+import AllSitesLink from './AllSitesLink.vue';
+import Matomo from '../Matomo/Matomo';
+import MatomoUrl from '../MatomoUrl/MatomoUrl';
+import translate from '../translate';
+import SitesStore, { Site } from './SitesStore';
+import debounce from '../debounce';
+
+interface SiteRef {
+ id: string|number;
+ name: string;
+}
+
+interface SiteSelectorState {
+ searchTerm: string;
+ showSitesList: boolean;
+ isLoading: boolean;
+ sites: Site[];
+ selectedSite: SiteRef;
+ autocompleteMinSites: null|number;
+}
+
+export default defineComponent({
+ props: {
+ modelValue: {
+ Object,
+ default: {
+ id: Matomo.idSite,
+ name: Matomo.helper.htmlDecode(Matomo.siteName),
+ },
+ },
+ showSelectedSite: {
+ type: Boolean,
+ default: false,
+ },
+ showAllSitesItem: {
+ type: Boolean,
+ default: true,
+ },
+ switchSiteOnSelect: {
+ type: Boolean,
+ default: true,
+ },
+ onlySitesWithAdminAccess: {
+ type: Boolean,
+ default: false,
+ },
+ name: {
+ type: String,
+ default: '',
+ },
+ allSitesText: {
+ type: String,
+ default: translate('General_MultiSitesSummary'),
+ },
+ allSitesLocation: {
+ type: String,
+ default: 'bottom',
+ },
+ placeholder: String,
+ },
+ emits: ['update:modelValue', 'blur'],
+ components: {
+ AllSitesLink,
+ },
+ directives: {
+ FocusAnywhereButHere,
+ FocusIf,
+ },
+ watch: {
+ modelValue: {
+ handler(newValue) {
+ this.selectedSite = { ...newValue };
+ },
+ deep: true,
+ },
+ },
+ data(): SiteSelectorState {
+ return {
+ searchTerm: '',
+ activeSiteId: Matomo.idSite,
+ showSitesList: false,
+ isLoading: false,
+ sites: [],
+ selectedSite: {
+ id: Matomo.idSite,
+ name: Matomo.helper.htmlDecode(Matomo.siteName),
+ },
+ autocompleteMinSites: parseInt(Matomo.config.autocomplete_min_sites as string, 10),
+ };
+ },
+ mounted() {
+ window.initTopControls();
+
+ this.loadInitialSites().then(() => {
+ if ((!this.selectedSite || !this.selectedSite.id) && this.sites[0]) {
+ this.selectedSite = { id: this.sites[0].idsite, name: this.sites[0].name };
+ this.$emit('update:modelValue', { ...this.selectedSite });
+ }
+ });
+
+ const shortcutTitle = translate('CoreHome_ShortcutWebsiteSelector');
+ Matomo.helper.registerShortcut('w', shortcutTitle, (event) => {
+ if (event.altKey) {
+ return;
+ }
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false; // IE
+ }
+ this.$refs.selectorLink.click();
+ this.$refs.selectorLink.focus();
+ });
+ },
+ created() {
+ this.onSearchInputKeydown = debounce(this.onSearchInputKeydown.bind(this));
+ },
+ computed: {
+ shouldFocusOnSearch() {
+ return (this.showSitesList && this.autocompleteMinSites <= this.sites.length)
+ || this.searchTerm;
+ },
+ selectorLinkTitle() {
+ return this.hasMultipleSites
+ ? translate('CoreHome_ChangeCurrentWebsite', this.selectedSite?.name || this.firstSiteName)
+ : '';
+ },
+ hasMultipleSites() {
+ return SitesStore.initialSites.value && SitesStore.initialSites.value.length > 1;
+ },
+ firstSiteName() {
+ return this.sites && this.sites.length > 0 ? this.sites[0].name : '';
+ },
+ urlAllSites() {
+ const newQuery = MatomoUrl.stringify({
+ ...MatomoUrl.urlParsed.value,
+ module: 'MultiSites',
+ action: 'index',
+ date: MatomoUrl.parsed.value.date,
+ period: MatomoUrl.parsed.value.period,
+ });
+ return `?${newQuery}`;
+ },
+ },
+ methods: {
+ onAllSitesClick(event: MouseEvent) {
+ this.switchSite({ idsite: 'all', name: this.allSitesText }, event);
+ this.showSitesList = false;
+ },
+ switchSite(site: SiteRef, event: KeyboardEvent|MouseEvent) {
+ // for Mac OS cmd key needs to be pressed, ctrl key on other systems
+ const controlKey = navigator.userAgent.indexOf('Mac OS X') !== -1 ? event.metaKey : event.ctrlKey;
+
+ if (event && controlKey && event.target && (event.target as HTMLLinkElement).href) {
+ window.open((event.target as HTMLLinkElement).href, '_blank');
+ return;
+ }
+
+ this.selectedSite = { id: site.idsite, name: site.name };
+ this.$emit('update:modelValue', { ...this.selectedSite });
+
+ if (!this.switchSiteOnSelect || this.activeSiteId === site.idsite) {
+ return;
+ }
+
+ SitesStore.loadSite(site.idsite);
+ },
+ onBlur() {
+ this.showSitesList = false;
+ this.$emit('blur');
+ },
+ onClickSelector() {
+ if (this.hasMultipleSites) {
+ this.showSitesList = !this.showSitesList;
+
+ if (!this.isLoading && !this.searchTerm) {
+ this.loadInitialSites();
+ }
+ }
+ },
+ onPressEnter(event: KeyboardEvent) {
+ if (event.key !== 'Enter') {
+ return;
+ }
+
+ event.preventDefault();
+
+ this.showSitesList = !this.showSitesList;
+ if (this.showSitesList && !this.isLoading) {
+ this.loadInitialSites();
+ }
+ },
+ onSearchInputKeydown() {
+ setTimeout(() => {
+ this.searchSite(this.searchTerm);
+ });
+ },
+ getMatchedSiteName(siteName: string) {
+ const index = siteName.toUpperCase().indexOf(this.searchTerm.toUpperCase());
+ if (index === -1) {
+ return Matomo.helper.htmlEntities(siteName);
+ }
+
+ const previousPart = Matomo.helper.htmlEntities(siteName.substring(0, index));
+ const lastPart = Matomo.helper.htmlEntities(
+ siteName.substring(index + this.searchTerm.length),
+ );
+
+ return `${previousPart}<span class="autocompleteMatched">${this.searchTerm}</span>${lastPart}`;
+ },
+ loadInitialSites() {
+ return SitesStore.loadInitialSites().then((sites) => {
+ this.sites = sites || [];
+ });
+ },
+ searchSite(term: string) {
+ this.isLoading = true;
+
+ SitesStore.searchSite(term, this.onlySitesWithAdminAccess).then((sites) => {
+ if (sites) {
+ this.sites = sites;
+ }
+ }).finally(() => {
+ this.isLoading = false;
+ });
+ },
+ getUrlForSiteId(idSite: string|number) {
+ const newQuery = MatomoUrl.stringify({
+ ...MatomoUrl.urlParsed.value,
+ segment: '',
+ idSite,
+ });
+
+ const newHash = MatomoUrl.stringify({
+ ...MatomoUrl.hashParsed.value,
+ segment: '',
+ idSite,
+ });
+
+ return `?${newQuery}#?${newHash}`;
+ },
+ },
+});
+</script>
diff --git a/plugins/CoreHome/vue/src/SiteSelector/SitesStore.ts b/plugins/CoreHome/vue/src/SiteSelector/SitesStore.ts
new file mode 100644
index 0000000000..655803843c
--- /dev/null
+++ b/plugins/CoreHome/vue/src/SiteSelector/SitesStore.ts
@@ -0,0 +1,131 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+import { reactive, computed, readonly } from 'vue';
+import AjaxHelper from '../AjaxHelper/AjaxHelper';
+import MatomoUrl from '../MatomoUrl/MatomoUrl';
+
+export interface Site {
+ idsite: string;
+ name: string;
+}
+
+interface SitesStoreState {
+ initialSites: Site[]|null;
+ isInitialized: boolean;
+}
+
+class SitesStore {
+ private state = reactive<SitesStoreState>({
+ initialSites: [],
+ isInitialized: false,
+ });
+
+ private currentRequest: AbortablePromise;
+
+ private limitRequest: AbortablePromise;
+
+ public readonly initialSites = computed(() => readonly(this.state.initialSites));
+
+ loadInitialSites(): Promise<Site[]> {
+ if (this.state.isInitialized) {
+ return Promise.resolve(readonly(this.state.initialSites));
+ }
+
+ return this.searchSite('%').then((sites) => {
+ this.state.isInitialized = true;
+ this.state.initialSites = sites;
+ return readonly(sites);
+ });
+ }
+
+ loadSite(idSite: number|string): void {
+ if (idSite === 'all') {
+ MatomoUrl.updateUrl({
+ ...MatomoUrl.urlParsed.value,
+ module: 'MultiSites',
+ action: 'index',
+ date: MatomoUrl.parsed.value.date,
+ period: MatomoUrl.parsed.value.period,
+ });
+ } else {
+ MatomoUrl.updateUrl({
+ ...MatomoUrl.parsed.value,
+ segment: '',
+ idSite,
+ });
+ }
+ }
+
+ searchSite(term, onlySitesWithAdminAccess = false): Promise<Site[]> {
+ if (!term) {
+ return this.loadInitialSites();
+ }
+
+ if (this.currentRequest) {
+ this.currentRequest.abort();
+ }
+
+ if (this.limitRequest) {
+ this.limitRequest.abort();
+ this.limitRequest = null;
+ }
+
+ if (!this.limitRequest) {
+ this.limitRequest = AjaxHelper.fetch({ method: 'SitesManager.getNumWebsitesToDisplayPerPage' });
+ }
+
+ return this.limitRequest.then((response) => {
+ const limit = response.value;
+
+ let methodToCall = 'SitesManager.getPatternMatchSites';
+ if (onlySitesWithAdminAccess) {
+ methodToCall = 'SitesManager.getSitesWithAdminAccess';
+ }
+
+ this.currentRequest = AjaxHelper.fetch({
+ method: methodToCall,
+ limit,
+ pattern: term,
+ });
+
+ return this.currentRequest;
+ }).then((response) => {
+ if (response) {
+ return this.processWebsitesList(response);
+ }
+
+ return null;
+ }).finally(() => {
+ this.currentRequest = null;
+ });
+ }
+
+ private processWebsitesList(response) {
+ let sites = response;
+
+ if (!sites || !sites.length) {
+ return [];
+ }
+
+ sites = sites.map((s) => ({
+ ...s,
+ name: s.group ? `[${s.group}] ${s.name}` : s.name,
+ }));
+
+ sites.sort((lhs, rhs) => {
+ if (lhs.name.toLowerCase() < rhs.name.toLowerCase()) {
+ return -1;
+ }
+ return lhs.name.toLowerCase() > rhs.name.toLowerCase() ? 1 : 0;
+ });
+
+ return sites;
+ }
+}
+
+export default new SitesStore();