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-11-19 17:18:57 +0300
committerGitHub <noreply@github.com>2021-11-19 17:18:57 +0300
commit5ae81f880dbbd7ab3fc0d223d7a9b4409ae548eb (patch)
treea6f43d9984f62ac579acf970334aae13346b817d /plugins/CoreHome/vue/src/QuickAccess/QuickAccess.vue
parent45d8528596500769ae2587b780cb43b5f7f303dd (diff)
[Vue] migrate siteselector directive and quick-access directive (#18292)
* migrating RateFeature and ReviewLinks + adding AjaxHelper.fetch utility method (all untested) * get ratefeature component to work, modify matomodialog component to use v-model, add event parameters to createAngularAdapter, allow translate to use variadic args or one string array + rebuild * remove ratefeature angularjs files * rebuild + make vue mapping property optional in createANgularJsAdapter * migrate enrichedheadline and get to work * fix test * fix translate * fix another translate issue & migrate contentblock directive * fix anchor links, not including the "/" causes angularjs to fail (also on 4.x-dev) * update expected screenshots * fix ui test * fix some test failures * fix nested transclude issue * remove content block files * fix icon spacing that occurs due to angularjs inserting empty comments in between nodes while vue 3 does not * update some screenshots * update screenshot (actually fixes an alignment issue) * update screenshot * first pass at converting comparisons service/component * get new code to build and load without error in the UI * debugging * getting basic functionaltiy to work * Update _dataTable.twig * fix UI test failure + URL encoding/angularjs issue causing back button to not work * fix order of operations issue * built vue files * using ref in setup() is not needed to access this.$refs * Convert comparisons service angularjs tests to comparison store typescript tests. * migrate piwik-date-picker directive * migrate date range picker component (changed invalid date in input handling to just reset back to the previous date since it was easier in vue to do that) * migrate period-date-picker component (using composition api more when easier for migration) * convert piwik-expand-onclick directive to vue directive * migrate expand on hover directive to vue directive * fix variable reference * build * Add materialize-css @types and migrate piwik-dropdown-menu. * migrate focus-anywhere-but-here directive to vue directive * migrate focus-if directive * migrate menudropdown directive * forgot to remove old files * built vue files * first pass at migrating notification directive, notification service and parts of UI.Notification to Vue * rewrite URL handling to use computed properties in a URL store + do the same for other dependent data in the comparison store to allow vues to subscribe to the properties for changes to global state * fix some tests * some more fixes * more fixes + disallow modifications to MatomoUrl state * get angularjs unit tests to pass + fix a couple more issues * another fix * fix bad merge * self review + fixes * remove old fix as it may not be needed anymore * empty string is not a valid date + do not report invalid date exception just rethrow * update screenshots and try to fix random failure * use jquery $destroy event instead of scope one since the scope one is broadcasted * rangeChange event must be triggered once on mount * initialize startDateText/endDateText correctly * use jquery $destroy event instead of angularjs one * built vue files * fix menudropdown.directive.js reference * load vue in installation/updater & correctly make focusanywherebuthere stateful * correctly implement stateful directives for ExpandOnClick/ExpandOnHover * less tweak (angularjs comment removal) * fix submenu check * quick type fix * load vue in installation workflow * add broadcast.js to Installation workflow + do not fail in pk_translate if no translations are loaded * update expected screenshots (spacing of arrow changed because of angularjs comment no longer being there) * start moving Notification class code to notifications store * fix prop type * fix html escaping * built vue files * get toast and other transitions to work + fix broken toast * move all of notification.js to NotificationStore * wait for angular to be initialized to post events to avoid loading race condition * get scroll to notification to work + get initialization of notification groups to work * correct unmount + remove notifications service file * fix some test failures * re add accidentally removed (?) file * remove no longer needed file * Add CoreHome UMD in CoreUpdater/Installation. * self review * fix type + add default value * remove file from JS list * fix test * fix UI tests * set correct type in users manager notification and allow scope values to be transformed in createAngularAdapter * start migrating siteselector * small addition * migrate rest of site selector code + make some breaking changes to function signatures in createAngularJsAdapter * disable webpack asset size hints/warnings + get siteselector code to build * fixing some bugs * fix some more issues (allow specifing require in createAngularJsAdapter and make AjaxHelper promises abortable) * get npm test to pass * a couple more fixes * remove existing files * convert quick-access directive and use shared code/state with site selector * remove site selector model * fix more issues and get UI tests to pass for quickaccess * remove debugging code / todo * fix initial value * add back a $timeout() * fixing tests, the post blur scope.$apply()s are apparently required for angularjs to function properly * fixing more UI test failures * rebuild * fix vue build * why were these deleted? * remove debug code * fix css class issue + update expected screenshots * rebuild CoreHome * revert styling change * built vue files * get focus-if to work and remove debugging return; * rebuilt vue * should not need to specify type there * built CoreHome * built vue files * apply review feedback * get auto clearing behavior to work in site selector * fix a couple more bugs * rebuild vue * escape htmle entities in site name * tweak Co-authored-by: sgiehl <stefan@matomo.org>
Diffstat (limited to 'plugins/CoreHome/vue/src/QuickAccess/QuickAccess.vue')
-rw-r--r--plugins/CoreHome/vue/src/QuickAccess/QuickAccess.vue465
1 files changed, 465 insertions, 0 deletions
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>