diff options
author | dizzy <diosmosis@users.noreply.github.com> | 2021-11-19 17:18:57 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-19 17:18:57 +0300 |
commit | 5ae81f880dbbd7ab3fc0d223d7a9b4409ae548eb (patch) | |
tree | a6f43d9984f62ac579acf970334aae13346b817d /plugins/CoreHome/vue/src/SiteSelector/SiteSelector.vue | |
parent | 45d8528596500769ae2587b780cb43b5f7f303dd (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/SiteSelector/SiteSelector.vue')
-rw-r--r-- | plugins/CoreHome/vue/src/SiteSelector/SiteSelector.vue | 370 |
1 files changed, 370 insertions, 0 deletions
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> |