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')
-rw-r--r--plugins/CoreHome/vue/src/AjaxHelper/AjaxHelper.ts14
-rw-r--r--plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts8
-rw-r--r--plugins/CoreHome/vue/src/MatomoUrl/MatomoUrl.ts12
-rw-r--r--plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.adapter.ts (renamed from plugins/CoreHome/vue/src/Menudropdown/Menudropdown.adapter.ts)4
-rw-r--r--plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.less (renamed from plugins/CoreHome/vue/src/Menudropdown/Menudropdown.less)0
-rw-r--r--plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.vue (renamed from plugins/CoreHome/vue/src/Menudropdown/Menudropdown.vue)0
-rw-r--r--plugins/CoreHome/vue/src/QuickAccess/QuickAccess.adapter.ts23
-rw-r--r--plugins/CoreHome/vue/src/QuickAccess/QuickAccess.less65
-rw-r--r--plugins/CoreHome/vue/src/QuickAccess/QuickAccess.vue465
-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
-rw-r--r--plugins/CoreHome/vue/src/createAngularJsAdapter.ts19
-rw-r--r--plugins/CoreHome/vue/src/debounce.ts19
-rw-r--r--plugins/CoreHome/vue/src/index.ts9
17 files changed, 1433 insertions, 13 deletions
diff --git a/plugins/CoreHome/vue/src/AjaxHelper/AjaxHelper.ts b/plugins/CoreHome/vue/src/AjaxHelper/AjaxHelper.ts
index f128ae6c32..a3965760b8 100644
--- a/plugins/CoreHome/vue/src/AjaxHelper/AjaxHelper.ts
+++ b/plugins/CoreHome/vue/src/AjaxHelper/AjaxHelper.ts
@@ -346,7 +346,7 @@ export default class AjaxHelper<T = any> { // eslint-disable-line
/**
* Send the request
*/
- send(): Promise<T> {
+ send(): AbortablePromise<T> {
if ($(this.errorElement).length) {
$(this.errorElement).hide();
}
@@ -358,7 +358,7 @@ export default class AjaxHelper<T = any> { // eslint-disable-line
this.requestHandle = this.buildAjaxCall();
window.globalAjaxQueue.push(this.requestHandle);
- return new Promise<T>((resolve, reject) => {
+ const result: AbortablePromise<T> = new Promise<T>((resolve, reject) => {
this.requestHandle!.then(resolve).fail((xhr: jqXHR) => {
if (xhr.statusText !== 'abort') {
console.log(`Warning: the ${$.param(this.getParams)} request failed!`);
@@ -366,7 +366,15 @@ export default class AjaxHelper<T = any> { // eslint-disable-line
reject(xhr);
}
});
- });
+ }) as AbortablePromise<T>;
+
+ result.abort = () => {
+ if (this.requestHandle) {
+ this.requestHandle.abort();
+ }
+ };
+
+ return result;
}
/**
diff --git a/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts b/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts
index ee03ce55c7..cd5965d1c6 100644
--- a/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts
+++ b/plugins/CoreHome/vue/src/MatomoDialog/MatomoDialog.adapter.ts
@@ -22,19 +22,19 @@ export default createAngularJsAdapter<[IParseService]>({
},
},
events: {
- yes: ($event, scope, element, attrs) => {
+ yes: ($event, vm, scope, element, attrs) => {
if (attrs.yes) {
scope.$eval(attrs.yes);
setTimeout(() => { scope.$apply(); }, 0);
}
},
- no: ($event, scope, element, attrs) => {
+ no: ($event, vm, scope, element, attrs) => {
if (attrs.no) {
scope.$eval(attrs.no);
setTimeout(() => { scope.$apply(); }, 0);
}
},
- validation: ($event, scope, element, attrs) => {
+ validation: ($event, vm, scope, element, attrs) => {
if (attrs.no) {
scope.$eval(attrs.no);
setTimeout(() => { scope.$apply(); }, 0);
@@ -46,7 +46,7 @@ export default createAngularJsAdapter<[IParseService]>({
setTimeout(() => { scope.$apply(); }, 0);
}
},
- 'update:modelValue': (newValue, scope, element, attrs, $parse: IParseService) => {
+ 'update:modelValue': (newValue, vm, scope, element, attrs, controller, $parse: IParseService) => {
setTimeout(() => {
scope.$apply($parse(attrs.piwikDialog).assign(scope, newValue));
}, 0);
diff --git a/plugins/CoreHome/vue/src/MatomoUrl/MatomoUrl.ts b/plugins/CoreHome/vue/src/MatomoUrl/MatomoUrl.ts
index 11eec228a8..02580be10a 100644
--- a/plugins/CoreHome/vue/src/MatomoUrl/MatomoUrl.ts
+++ b/plugins/CoreHome/vue/src/MatomoUrl/MatomoUrl.ts
@@ -68,6 +68,18 @@ class MatomoUrl {
$location.search(serializedParams);
}
+ updateUrl(params: QueryParameters|string, hashParams: QueryParameters|string = {}) {
+ const serializedParams: string = typeof params !== 'string' ? this.stringify(params) : params;
+ const serializedHashParams: string = typeof hashParams !== 'string' ? this.stringify(hashParams) : hashParams;
+
+ let url = `?${serializedParams}`;
+ if (serializedHashParams.length) {
+ url = `${url}#?${serializedHashParams}`;
+ }
+
+ window.broadcast.propagateNewPage('', undefined, undefined, undefined, url);
+ }
+
getSearchParam(paramName: string): string {
const hash = window.location.href.split('#');
diff --git a/plugins/CoreHome/vue/src/Menudropdown/Menudropdown.adapter.ts b/plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.adapter.ts
index f56616f54d..9fc8ae8e13 100644
--- a/plugins/CoreHome/vue/src/Menudropdown/Menudropdown.adapter.ts
+++ b/plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.adapter.ts
@@ -6,10 +6,10 @@
*/
import createAngularJsAdapter from '../createAngularJsAdapter';
-import Menudropdown from './Menudropdown.vue';
+import MenuDropdown from './MenuDropdown.vue';
export default createAngularJsAdapter({
- component: Menudropdown,
+ component: MenuDropdown,
scope: {
menuTitle: {
angularJsBind: '@',
diff --git a/plugins/CoreHome/vue/src/Menudropdown/Menudropdown.less b/plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.less
index 2a04d675df..2a04d675df 100644
--- a/plugins/CoreHome/vue/src/Menudropdown/Menudropdown.less
+++ b/plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.less
diff --git a/plugins/CoreHome/vue/src/Menudropdown/Menudropdown.vue b/plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.vue
index fdd26bfdba..fdd26bfdba 100644
--- a/plugins/CoreHome/vue/src/Menudropdown/Menudropdown.vue
+++ b/plugins/CoreHome/vue/src/MenuDropdown/MenuDropdown.vue
diff --git a/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.adapter.ts b/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.adapter.ts
new file mode 100644
index 0000000000..2818deff0a
--- /dev/null
+++ b/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.adapter.ts
@@ -0,0 +1,23 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+import { ITimeoutService } from 'angular';
+import createAngularJsAdapter from '../createAngularJsAdapter';
+import QuickAccess from './QuickAccess.vue';
+
+export default createAngularJsAdapter<[ITimeoutService]>({
+ component: QuickAccess,
+ directiveName: 'piwikQuickAccess',
+ events: {
+ itemSelected(event, vm, scope, elem, attrs, controller, $timeout: ITimeoutService) {
+ $timeout();
+ },
+ blur(event, vm, scope) {
+ setTimeout(() => scope.$apply());
+ },
+ },
+});
diff --git a/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.less b/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.less
new file mode 100644
index 0000000000..3bad77b9ba
--- /dev/null
+++ b/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.less
@@ -0,0 +1,65 @@
+.quick-access {
+ position: relative;
+
+ &:hover,
+ &.expanded,
+ &.active {
+ input {
+ background-color: @theme-color-background-contrast !important;
+ }
+ }
+ li {
+ font-size: 11px;
+ }
+
+ li a {
+ padding: 10px 19px;
+ display: inline-block;
+ text-decoration: none;
+ word-break: break-all;
+ }
+
+ .icon-search {
+ position: absolute;
+ font-size: 14px;
+ top: 10px;
+ left: 10px;
+
+ }
+ input {
+ width:100%;
+ height: 100%;
+ box-shadow: 0 0 !important;
+ border-radius: 0 !important;
+ background-color: @theme-color-background-base !important;
+ font-size: 11px;
+ &:focus {
+ outline: none;
+ }
+ }
+ .selected {
+ background-color: @theme-color-background-tinyContrast !important;
+ }
+ .quick-access-category {
+ text-align: left !important;
+ font-size: 11px;
+ padding: 5px 5px 5px 10px;
+ cursor: pointer;
+ }
+ .result {
+ cursor: pointer;
+ }
+ .quick-access-category:hover {
+ background: none !important;
+ }
+ .no-result {
+ padding: 10px 19px;
+ cursor: default;
+ }
+ .websiteCategory {
+ cursor: default;
+ }
+ li.quick-access-help a {
+ word-break: break-word;
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.vue b/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.vue
new file mode 100644
index 0000000000..20c8588fc3
--- /dev/null
+++ b/plugins/CoreHome/vue/src/QuickAccess/QuickAccess.vue
@@ -0,0 +1,465 @@
+<!--
+ 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
+ ref="root"
+ class="quickAccessInside"
+ v-focus-anywhere-but-here="{ blur: onBlur }"
+ >
+ <span
+ class="icon-search"
+ @mouseenter="searchActive = true"
+ v-show="!(searchTerm || searchActive)"
+ />
+ <input
+ class="s"
+ @keydown="onKeypress($event)"
+ @focus="searchActive = true"
+ v-model="searchTerm"
+ type="text"
+ tabindex="2"
+ v-focus-if:[searchActive]="{}"
+ :title="quickAccessTitle"
+ />
+ <div
+ class="dropdown"
+ v-show="searchTerm && searchActive"
+ >
+ <ul v-show="!(numMenuItems > 0 || sites.length)">
+ <li class="no-result">{{ translate('General_SearchNoResults') }}</li>
+ </ul>
+ <ul v-for="subcategory in menuItems" :key="subcategory.title">
+ <li
+ class="quick-access-category"
+ @click="searchTerm = subcategory.title;searchMenu(searchTerm)"
+ >
+ {{ subcategory.title }}
+ </li>
+ <li
+ class="result"
+ :class="{ selected: submenuEntry.menuIndex === searchIndex }"
+ @mouseenter="searchIndex = submenuEntry.menuIndex"
+ @click="selectMenuItem(submenuEntry.index)"
+ v-for="submenuEntry in subcategory.items"
+ :key="submenuEntry.index"
+ >
+ <a>{{ submenuEntry.name.trim() }}</a>
+ </li>
+ </ul>
+ <ul class="quickAccessMatomoSearch">
+ <li
+ class="quick-access-category websiteCategory"
+ v-show="hasSitesSelector && sites.length || isLoading"
+ >
+ {{ translate('SitesManager_Sites') }}
+ </li>
+ <li
+ class="no-result"
+ v-show="hasSitesSelector && isLoading"
+ >
+ {{ translate('MultiSites_LoadingWebsites') }}
+ </li>
+ <li
+ class="result"
+ v-for="(site, index) in sites"
+ v-show="hasSitesSelector && !isLoading"
+ @mouseenter="searchIndex = numMenuItems + index"
+ :class="{ selected: numMenuItems + index === searchIndex }"
+ @click="selectSite(site.idsite)"
+ :key="site.idsite"
+ >
+ <a v-text="site.name" />
+ </li>
+ </ul>
+ <ul>
+ <li class="quick-access-category helpCategory">{{ translate('General_HelpResources') }}</li>
+ <li
+ :class="{ selected: searchIndex === 'help' }"
+ class="quick-access-help"
+ @mouseenter="searchIndex = 'help'"
+ >
+ <a
+ :href="`https://matomo.org?s=${encodeURIComponent(searchTerm)}`"
+ target="_blank"
+ >
+ {{ translate('CoreHome_SearchOnMatomo', searchTerm) }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import FocusAnywhereButHere from '../FocusAnywhereButHere/FocusAnywhereButHere';
+import FocusIf from '../FocusIf/FocusIf';
+import translate from '../translate';
+import SitesStore, { Site } from '../SiteSelector/SitesStore';
+import Matomo from '../Matomo/Matomo';
+import debounce from '../debounce';
+
+interface SubMenuItem {
+ name: string;
+ index: number;
+ category: string;
+}
+
+interface MenuItem {
+ title: string;
+ items: SubMenuItem[];
+}
+
+interface QuickAccessState {
+ menuItems: Array<unknown>;
+ numMenuItems: number;
+ searchActive: boolean;
+ searchTerm: string;
+ searchIndex: number;
+
+ menuIndexCounter: number;
+ readonly topMenuItems: SubMenuItem[];
+ readonly leftMenuItems: SubMenuItem[];
+ readonly segmentItems: SubMenuItem[];
+ readonly hasSegmentSelector: boolean;
+
+ sites: Site[];
+ isLoading: boolean;
+}
+
+function isElementInViewport(element: HTMLElement) {
+ const rect = element.getBoundingClientRect();
+
+ return rect.top >= 0
+ && rect.left >= 0
+ && rect.bottom <= window.$(window).height()
+ && rect.right <= window.$(window).width();
+}
+
+function scrollFirstElementIntoView(element: HTMLElement) {
+ if (element && element.scrollIntoView) {
+ // make sure search is visible
+ element.scrollIntoView();
+ }
+}
+
+export default defineComponent({
+ props: {
+ },
+ directives: {
+ FocusAnywhereButHere,
+ FocusIf,
+ },
+ watch: {
+ searchActive(newValue) {
+ const classes = this.$refs.root.parentElement.classList;
+ classes.toggle('active', newValue);
+ classes.toggle('expanded', newValue);
+ },
+ },
+ mounted() {
+ // TODO: temporary, remove after angularjs is removed.
+ // this is currently needed since angularjs will render a div, then vue will render a div
+ // within it, but the top controls and CSS expect to have certain CSS classes in the root
+ // element.
+ // same applies to above watch for searchActive()
+ this.$refs.root.parentElement.classList.add('quick-access', 'piwikSelector');
+
+ if (typeof window.initTopControls !== 'undefined' && window.initTopControls) {
+ window.initTopControls();
+ }
+
+ Matomo.helper.registerShortcut('f', translate('CoreHome_ShortcutSearch'), (event) => {
+ if (event.altKey) {
+ return;
+ }
+
+ event.preventDefault();
+
+ scrollFirstElementIntoView(this.$refs.root);
+
+ this.activateSearch();
+ });
+ },
+ data(): QuickAccessState {
+ const hasSegmentSelector = !!document.querySelector('.segmentEditorPanel');
+
+ return {
+ menuItems: [],
+ numMenuItems: 0,
+ searchActive: false,
+ searchTerm: '',
+ searchIndex: 0,
+ menuIndexCounter: -1,
+ topMenuItems: null,
+ leftMenuItems: null,
+ segmentItems: null,
+ hasSegmentSelector,
+ sites: [],
+ isLoading: false,
+ };
+ },
+ created() {
+ this.searchMenu = debounce(this.searchMenu.bind(this));
+ },
+ computed: {
+ hasSitesSelector() {
+ return !!document.querySelector('.top_controls [piwik-siteselector]');
+ },
+ quickAccessTitle() {
+ let searchAreasTitle = '';
+ const searchAreas = [translate('CoreHome_MenuEntries')];
+
+ if (this.hasSegmentSelector) {
+ searchAreas.push(translate('CoreHome_Segments'));
+ }
+
+ if (this.hasSitesSelector) {
+ searchAreas.push(translate('SitesManager_Sites'));
+ }
+
+ while (searchAreas.length) {
+ searchAreasTitle += searchAreas.shift();
+ if (searchAreas.length >= 2) {
+ searchAreasTitle += ', ';
+ } else if (searchAreas.length === 1) {
+ searchAreasTitle += ` ${translate('General_And')} `;
+ }
+ }
+
+ return translate('CoreHome_QuickAccessTitle', searchAreasTitle);
+ },
+ },
+ emits: ['itemSelected', 'blur'],
+ methods: {
+ onKeypress(event) {
+ const areSearchResultsDisplayed = this.searchTerm && this.searchActive;
+ const isTabKey = event.which === 9;
+ const isEscKey = event.which === 27;
+
+ if (event.which === 38) {
+ this.highlightPreviousItem();
+ event.preventDefault();
+ } else if (event.which === 40) {
+ this.highlightNextItem();
+ event.preventDefault();
+ } else if (event.which === 13) {
+ this.clickQuickAccessMenuItem();
+ } else if (isTabKey && areSearchResultsDisplayed) {
+ this.deactivateSearch();
+ } else if (isEscKey && areSearchResultsDisplayed) {
+ this.deactivateSearch();
+ } else {
+ setTimeout(() => {
+ this.searchActive = true;
+ this.searchMenu(this.searchTerm);
+ });
+ }
+ },
+ highlightPreviousItem() {
+ if ((this.searchIndex - 1) < 0) {
+ this.searchIndex = 0;
+ } else {
+ this.searchIndex -= 1;
+ }
+ this.makeSureSelectedItemIsInViewport();
+ },
+ highlightNextItem() {
+ const numTotal = (this.$refs.root as HTMLElement).querySelectorAll('li.result').length;
+
+ if (numTotal <= (this.searchIndex + 1)) {
+ this.searchIndex = numTotal - 1;
+ } else {
+ this.searchIndex += 1;
+ }
+
+ this.makeSureSelectedItemIsInViewport();
+ },
+ clickQuickAccessMenuItem() {
+ const selectedMenuElement = this.getCurrentlySelectedElement();
+ if (selectedMenuElement) {
+ setTimeout(() => {
+ selectedMenuElement.click();
+ this.$emit('itemSelected', selectedMenuElement);
+ }, 20);
+ }
+ },
+ deactivateSearch() {
+ this.searchTerm = '';
+ this.searchActive = false;
+ (this.$refs.root).querySelector('input').blur();
+ },
+ makeSureSelectedItemIsInViewport() {
+ const element = this.getCurrentlySelectedElement();
+
+ if (element && !isElementInViewport(element)) {
+ scrollFirstElementIntoView(element);
+ }
+ },
+ getCurrentlySelectedElement() {
+ const results = (this.$refs.root as HTMLElement).querySelectorAll('li.result');
+ if (results && results.length && results.item(this.searchIndex)) {
+ return results.item(this.searchIndex);
+ }
+ return null;
+ },
+ searchMenu(unprocessedSearchTerm: string) {
+ const searchTerm = unprocessedSearchTerm.toLowerCase();
+
+ let index = -1;
+ const menuItemsIndex: Record<string, number> = {};
+ const menuItems: MenuItem[] = [];
+
+ const moveToCategory = (theSubmenuItem) => {
+ // force rerender of element to prevent weird side effects
+ const submenuItem = { ...theSubmenuItem };
+ // needed for proper highlighting with arrow keys
+ index += 1;
+ submenuItem.menuIndex = index;
+
+ const { category } = submenuItem;
+ if (!(category in menuItemsIndex)) {
+ menuItems.push({ title: category, items: [] });
+ menuItemsIndex[category] = menuItems.length - 1;
+ }
+
+ const indexOfCategory = menuItemsIndex[category];
+ menuItems[indexOfCategory].items.push(submenuItem);
+ };
+
+ this.resetSearchIndex();
+
+ if (this.hasSitesSelector) {
+ this.isLoading = true;
+ SitesStore.searchSite(searchTerm).then((sites) => {
+ this.sites = sites;
+ }).finally(() => {
+ this.isLoading = false;
+ });
+ }
+
+ const menuItemMatches = (i) => i.name.toLowerCase().indexOf(searchTerm) !== -1
+ || i.category.toLowerCase().indexOf(searchTerm) !== -1;
+
+ // get the menu items on first search since this component can be mounted
+ // before the menus are
+ if (this.topMenuItems === null) {
+ this.topMenuItems = this.getTopMenuItems();
+ }
+ if (this.leftMenuItems === null) {
+ this.leftMenuItems = this.getLeftMenuItems();
+ }
+ if (this.segmentItems === null) {
+ this.segmentItems = this.getSegmentItems();
+ }
+
+ const topMenuItems = this.topMenuItems.filter(menuItemMatches);
+ const leftMenuItems = this.leftMenuItems.filter(menuItemMatches);
+ const segmentItems = this.segmentItems.filter(menuItemMatches);
+
+ topMenuItems.forEach(moveToCategory);
+ leftMenuItems.forEach(moveToCategory);
+ segmentItems.forEach(moveToCategory);
+
+ this.numMenuItems = topMenuItems.length + leftMenuItems.length + segmentItems.length;
+ this.menuItems = menuItems;
+ },
+ resetSearchIndex() {
+ this.searchIndex = 0;
+ this.makeSureSelectedItemIsInViewport();
+ },
+ selectSite(idSite: string|number) {
+ SitesStore.loadSite(idSite);
+ },
+ selectMenuItem(index: number) {
+ const target: HTMLElement = document.querySelector(`[quick_access='${index}']`);
+ if (target) {
+ this.deactivateSearch();
+
+ const href = target.getAttribute('href');
+ if (href && href.length > 10 && target && target.click) {
+ try {
+ target.click();
+ } catch (e) {
+ window.$(target).click();
+ }
+ } else {
+ // not sure why jquery is used here and above, but only sometimes. keeping for BC.
+ window.$(target).click();
+ }
+ }
+ },
+ onBlur() {
+ this.searchActive = false;
+ this.$emit('blur');
+ },
+ activateSearch() {
+ this.searchActive = true;
+ },
+ getTopMenuItems() {
+ const category = translate('CoreHome_Menu');
+
+ const topMenuItems: SubMenuItem[] = [];
+ document.querySelectorAll('nav .sidenav li > a').forEach((element) => {
+ let text = element.textContent.trim();
+
+ if (!text) {
+ text = element.getAttribute('title').trim(); // possibly a icon, use title instead
+ }
+
+ if (text) {
+ topMenuItems.push({ name: text, index: this.menuIndexCounter += 1, category });
+ element.setAttribute('quick_access', `${this.menuIndexCounter}`);
+ }
+ });
+
+ return topMenuItems;
+ },
+ getLeftMenuItems() {
+ const leftMenuItems: SubMenuItem[] = [];
+
+ document.querySelectorAll('#secondNavBar .menuTab').forEach((element) => {
+ let category = window.$(element).find('> .item').text().trim();
+
+ if (category && category.lastIndexOf('\n') !== -1) {
+ // remove "\n\nMenu"
+ category = category.substr(0, category.lastIndexOf('\n')).trim();
+ }
+
+ window.$(element).find('li .item').each((i, subElement) => {
+ const text = subElement.textContent.trim();
+ if (text) {
+ leftMenuItems.push({ name: text, category, index: this.menuIndexCounter += 1 });
+ subElement.setAttribute('quick_access', `${this.menuIndexCounter}`);
+ }
+ });
+ });
+
+ return leftMenuItems;
+ },
+ getSegmentItems() {
+ if (!this.hasSegmentSelector) {
+ return [];
+ }
+
+ const category = translate('CoreHome_Segments');
+
+ const segmentItems: SubMenuItem[] = [];
+ document.querySelectorAll('.segmentList [data-idsegment]').forEach((element) => {
+ const text = element.querySelector('.segname').textContent.trim();
+
+ if (text) {
+ segmentItems.push({ name: text, category, index: this.menuIndexCounter += 1 });
+ element.setAttribute('quick_access', `${this.menuIndexCounter}`);
+ }
+ });
+
+ return segmentItems;
+ },
+ },
+});
+</script>
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();
diff --git a/plugins/CoreHome/vue/src/createAngularJsAdapter.ts b/plugins/CoreHome/vue/src/createAngularJsAdapter.ts
index b28ee95d33..d1806e84b5 100644
--- a/plugins/CoreHome/vue/src/createAngularJsAdapter.ts
+++ b/plugins/CoreHome/vue/src/createAngularJsAdapter.ts
@@ -31,9 +31,11 @@ type AdapterFunction<InjectTypes, R = void> = (
type EventAdapterFunction<InjectTypes, R = void> = (
$event: any, // eslint-disable-line
+ vm: ComponentPublicInstance,
scope: ng.IScope,
element: ng.IAugmentedJQuery,
attrs: ng.IAttributes,
+ otherController: ng.IControllerService,
...injected: InjectTypes,
) => R;
@@ -42,6 +44,7 @@ type PostCreateFunction<InjectTypes, R = void> = (
scope: ng.IScope,
element: ng.IAugmentedJQuery,
attrs: ng.IAttributes,
+ otherController: ng.IControllerService,
...injected: InjectTypes,
) => R;
@@ -63,6 +66,7 @@ function toAngularJsCamelCase(arg: string): string {
export default function createAngularJsAdapter<InjectTypes = []>(options: {
component: ComponentType,
+ require?: string,
scope?: ScopeMapping,
directiveName: string,
events?: EventMapping<InjectTypes>,
@@ -75,6 +79,7 @@ export default function createAngularJsAdapter<InjectTypes = []>(options: {
}): ng.IDirectiveFactory {
const {
component,
+ require,
scope = {},
events = {},
$inject,
@@ -104,6 +109,7 @@ export default function createAngularJsAdapter<InjectTypes = []>(options: {
function angularJsAdapter(...injectedServices: InjectTypes) {
const adapter: ng.IDirective = {
restrict,
+ require,
scope: noScope ? undefined : angularJsScope,
compile: function angularJsAdapterCompile() {
return {
@@ -111,6 +117,7 @@ export default function createAngularJsAdapter<InjectTypes = []>(options: {
ngScope: ng.IScope,
ngElement: ng.IAugmentedJQuery,
ngAttrs: ng.IAttributes,
+ ngController: ng.IControllerService,
) {
const clone = transclude ? ngElement.find(`[ng-transclude][counter=${currentTranscludeCounter}]`) : null;
@@ -173,7 +180,15 @@ export default function createAngularJsAdapter<InjectTypes = []>(options: {
}
if (events[name]) {
- events[name]($event, ngScope, ngElement, ngAttrs, ...injectedServices);
+ events[name](
+ $event,
+ this,
+ ngScope,
+ ngElement,
+ ngAttrs,
+ ngController,
+ ...injectedServices,
+ );
}
},
},
@@ -213,7 +228,7 @@ export default function createAngularJsAdapter<InjectTypes = []>(options: {
}
if (postCreate) {
- postCreate(vm, ngScope, ngElement, ngAttrs, ...injectedServices);
+ postCreate(vm, ngScope, ngElement, ngAttrs, ngController, ...injectedServices);
}
ngElement.on('$destroy', () => {
diff --git a/plugins/CoreHome/vue/src/debounce.ts b/plugins/CoreHome/vue/src/debounce.ts
new file mode 100644
index 0000000000..b92fe93a62
--- /dev/null
+++ b/plugins/CoreHome/vue/src/debounce.ts
@@ -0,0 +1,19 @@
+interface Callable {
+ (...args: unknown[]): void;
+}
+
+const DEFAULT_DEBOUNCE_DELAY = 300;
+
+export default function debounce<F extends Callable>(fn: F, delayInMs = DEFAULT_DEBOUNCE_DELAY): F {
+ let timeout: ReturnType<typeof setTimeout>;
+
+ return (...args: Parameters<F>) => {
+ if (timeout) {
+ clearTimeout(timeout);
+ }
+
+ timeout = setTimeout(() => {
+ fn(...args);
+ }, delayInMs);
+ };
+}
diff --git a/plugins/CoreHome/vue/src/index.ts b/plugins/CoreHome/vue/src/index.ts
index baaada6b9a..aade352c11 100644
--- a/plugins/CoreHome/vue/src/index.ts
+++ b/plugins/CoreHome/vue/src/index.ts
@@ -24,11 +24,14 @@ import './MatomoDialog/MatomoDialog.adapter';
import './EnrichedHeadline/EnrichedHeadline.adapter';
import './ContentBlock/ContentBlock.adapter';
import './Comparisons/Comparisons.adapter';
-import './Menudropdown/Menudropdown.adapter';
+import './MenuDropdown/MenuDropdown.adapter';
import './DatePicker/DatePicker.adapter';
import './DateRangePicker/DateRangePicker.adapter';
import './PeriodDatePicker/PeriodDatePicker.adapter';
+import './SiteSelector/SiteSelector.adapter';
+import './QuickAccess/QuickAccess.adapter';
+export { default as debounce } from './debounce';
export { default as createAngularJsAdapter } from './createAngularJsAdapter';
export { default as activityIndicatorAdapter } from './ActivityIndicator/ActivityIndicator.adapter';
export { default as ActivityIndicator } from './ActivityIndicator/ActivityIndicator.vue';
@@ -48,8 +51,10 @@ export { default as ExpandOnHover } from './ExpandOnHover/ExpandOnHover';
export { default as EnrichedHeadline } from './EnrichedHeadline/EnrichedHeadline.vue';
export { default as ContentBlock } from './ContentBlock/ContentBlock.vue';
export { default as Comparisons } from './Comparisons/Comparisons.vue';
-export { default as Menudropdown } from './Menudropdown/Menudropdown.vue';
+export { default as MenuDropdown } from './MenuDropdown/MenuDropdown.vue';
export { default as DatePicker } from './DatePicker/DatePicker.vue';
export { default as DateRangePicker } from './DateRangePicker/DateRangePicker.vue';
export { default as PeriodDatePicker } from './PeriodDatePicker/PeriodDatePicker.vue';
export * from './Notification';
+export { default as SiteSelector } from './SiteSelector/SiteSelector.vue';
+export { default as QuickAccess } from './QuickAccess/QuickAccess.vue';