diff --git a/frontend/package.json b/frontend/package.json index 6663a3db8a..549cbed432 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,9 +6,9 @@ "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", - "lint": "eslint src/**/*.{ts,js,vue} cypress/tests/**/*.js cypress/support.js && stylelint ./src/**/*.{vue,scss} && npm run puglint", + "lint": "eslint src/**/*.{ts,vue} cypress/tests/**/*.js cypress/support.js && stylelint ./src/**/*.{vue,scss} && npm run puglint", "devbuild": "vue-cli-service build --mode development", - "lintfix": "eslint --fix src/**/*.{ts,js,vue} cypress/tests/**/*.js cypress/support.js && stylelint --fix ./src/**/*.{vue,scss}", + "lintfix": "eslint --fix src/**/*.{ts,vue} cypress/tests/**/*.js cypress/support.js && stylelint --fix ./src/**/*.{vue,scss}", "puglint": "pug-lint-vue src", "serveOpen": "vue-cli-service serve --open" }, diff --git a/frontend/src/components/c-ramp.vue b/frontend/src/components/c-ramp.vue index 5ca70a7b81..7440e6d2e1 100644 --- a/frontend/src/components/c-ramp.vue +++ b/frontend/src/components/c-ramp.vue @@ -142,8 +142,15 @@ export default defineComponent({ } const zoomUser = { ...user }; - // Type cast here is unsafe - zoomUser.commits = user.dailyCommits as Commit[]; + // Calculate total commit result insertion and deletion for the daily/weekly commit selected + zoomUser.commits = user.dailyCommits.map( + (dailyCommit) => ({ + insertions: dailyCommit.commitResults.reduce((acc, currCommitResult) => acc + currCommitResult.insertions, 0), + deletions: dailyCommit.commitResults.reduce((acc, currCommitResult) => acc + currCommitResult.deletions, 0), + ...dailyCommit, + commitResults: dailyCommit.commitResults.map((commitResult) => ({ ...commitResult, isOpen: true })), + }), + ) as Commit[]; const info = { zRepo: user.repoName, diff --git a/frontend/src/components/c-summary-charts.vue b/frontend/src/components/c-summary-charts.vue index ea52675ddb..5569c73397 100644 --- a/frontend/src/components/c-summary-charts.vue +++ b/frontend/src/components/c-summary-charts.vue @@ -21,47 +21,64 @@ v-bind:class=" { warn: repo[0].name === '-' }" ) {{ getAuthorDisplayName(repo) }} ({{ repo[0].name }}) .summary-charts__title--contribution - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-total-contribution`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-total-contribution`)" + ) | [{{ getGroupTotalContribution(repo) }} lines] span.tooltip-text( v-if="filterGroupSelection === 'groupByRepos' && !isChartGroupWidgetMode" - ) Total contribution of group + )(v-bind:ref="`summary-charts-${i}-total-contribution`") Total contribution of group span.tooltip-text( v-else-if="filterGroupSelection === 'groupByAuthors' && !isChartGroupWidgetMode" - ) Total contribution of author + )(v-bind:ref="`summary-charts-${i}-total-contribution`") Total contribution of author a( v-if="!isGroupMerged(getGroupName(repo)) && !isChartGroupWidgetMode", v-on:click="handleMergeGroup(getGroupName(repo))" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-merge-group`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-merge-group`)" + ) font-awesome-icon.icon-button(:icon="['fas', 'chevron-up']") - span.tooltip-text Click to merge group + span.tooltip-text(v-bind:ref="`summary-charts-${i}-merge-group`") Click to merge group a( v-if="isGroupMerged(getGroupName(repo)) && !isChartGroupWidgetMode", v-on:click="handleExpandGroup(getGroupName(repo))" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-expand-group`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-expand-group`)" + ) font-awesome-icon.icon-button(:icon="['fas', 'chevron-down']") - span.tooltip-text Click to expand group + span.tooltip-text(v-bind:ref="`summary-charts-${i}-expand-group`") Click to expand group a( v-if="filterGroupSelection === 'groupByRepos'", v-bind:class="!isBrokenLink(getRepoLink(repo[0])) ? '' : 'broken-link'", v-bind:href="getRepoLink(repo[0])", target="_blank" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-repo-link`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-repo-link`)" + ) font-awesome-icon.icon-button(:icon="getRepoIcon(repo[0])") span.tooltip-text( v-if="!isChartGroupWidgetMode", + v-bind:ref="`summary-charts-${i}-repo-link`" ) {{getGroupRepoLinkMessage(repo[0])}} a( v-else-if="filterGroupSelection === 'groupByAuthors'", v-bind:class="!isBrokenLink(getAuthorProfileLink(repo[0], repo[0].name)) ? '' : 'broken-link'", v-bind:href="getAuthorProfileLink(repo[0], repo[0].name)", target="_blank" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-author-link`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-author-link`)" + ) font-awesome-icon.icon-button(icon="user") span.tooltip-text( v-if="!isChartGroupWidgetMode", + v-bind:ref="`summary-charts-${i}-author-link`" ) {{getAuthorProfileLinkMessage(repo[0])}} template(v-if="isGroupMerged(getGroupName(repo))") a( @@ -69,41 +86,54 @@ onclick="deactivateAllOverlays()", v-on:click="openTabAuthorship(repo[0], repo, 0, isGroupMerged(getGroupName(repo)))" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-group-code`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-group-code`)" + ) font-awesome-icon.icon-button( icon="code", v-bind:class="{ 'active-icon': isSelectedTab(repo[0].name, repo[0].repoName, 'authorship', true) }" ) - span.tooltip-text Click to view group's code + span.tooltip-text(v-bind:ref="`summary-charts-${i}-group-code`") Click to view group's code a( v-if="!isChartGroupWidgetMode", onclick="deactivateAllOverlays()", v-on:click="openTabZoom(repo[0], filterSinceDate, filterUntilDate, isGroupMerged(getGroupName(repo)))" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-commit-breakdown`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-commit-breakdown`)" + ) font-awesome-icon.icon-button( icon="list-ul", v-bind:class="{ 'active-icon': isSelectedTab(repo[0].name, repo[0].repoName, 'zoom', true) }" ) - span.tooltip-text Click to view breakdown of commits + span.tooltip-text(v-bind:ref="`summary-charts-${i}-commit-breakdown`") Click to view breakdown of commits a( v-if="isChartGroupWidgetMode && !isChartWidgetMode", v-bind:href="getReportLink()", target="_blank" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`summary-charts-${i}-commit-breakdown-group-widget`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-commit-breakdown-group-widget`)" + ) font-awesome-icon.icon-button( icon="arrow-up-right-from-square", ) span.tooltip-text( v-if="!isChartGroupWidgetMode", + v-bind:ref="`summary-charts-${i}-commit-breakdown-group-widget`" ) Click to view breakdown of commits on RepoSense a( v-if="!isChartGroupWidgetMode", v-on:click="getEmbeddedIframe(i)" ) - .tooltip(v-bind:id="'tooltip-' + i") + .tooltip(v-bind:id="'tooltip-' + i", + v-on:mouseover="onTooltipHover(`summary-charts-${i}-copy-iframe`)", + v-on:mouseout="resetTooltip(`summary-charts-${i}-copy-iframe`)" + ) font-awesome-icon.icon-button(icon="clipboard") - span.tooltip-text Click to copy iframe link for group + span.tooltip-text(v-bind:ref="`summary-charts-${i}-copy-iframe`") Click to copy iframe link for group .tooltip.summary-chart__title--percentile( v-if="sortGroupSelection.includes('totalCommits')" @@ -143,61 +173,87 @@ v-bind:class="!isBrokenLink(getRepoLink(user)) ? '' : 'broken-link'", v-bind:href="getRepoLink(user)", target="_blank" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`repo-${i}-author-${j}-repo-link`)", + v-on:mouseout="resetTooltip(`repo-${i}-author-${j}-repo-link`)" + ) font-awesome-icon.icon-button(:icon="getRepoIcon(repo[0])") span.tooltip-text( v-if="!isChartGroupWidgetMode", + v-bind:ref="`repo-${i}-author-${j}-repo-link`" ) {{getRepoLinkMessage(user)}} a( v-if="filterGroupSelection !== 'groupByAuthors'", v-bind:class="!isBrokenLink(getAuthorProfileLink(user, user.name)) ? '' : 'broken-link'", v-bind:href="getAuthorProfileLink(user, user.name)", target="_blank" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`repo-${i}-author-${j}-author-link`)", + v-on:mouseout="resetTooltip(`repo-${i}-author-${j}-author-link`)" + ) font-awesome-icon.icon-button(icon="user") span.tooltip-text( v-if="!isChartGroupWidgetMode", + v-bind:ref="`repo-${i}-author-${j}-author-link`" ) {{getAuthorProfileLinkMessage(user)}} a( v-if="!isChartGroupWidgetMode", onclick="deactivateAllOverlays()", v-on:click="openTabAuthorship(user, repo, j, isGroupMerged(getGroupName(repo)))" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`repo-${i}-author-${j}-author-contribution`)", + v-on:mouseout="resetTooltip(`repo-${i}-author-${j}-author-contribution`)" + ) font-awesome-icon.icon-button( icon="code", v-bind:class="{ 'active-icon': isSelectedTab(user.name, user.repoName, 'authorship', false) }" ) - span.tooltip-text Click to view author's contribution. + span.tooltip-text( + v-bind:ref="`repo-${i}-author-${j}-author-contribution`" + ) Click to view author's contribution. a( v-if="!isChartGroupWidgetMode", onclick="deactivateAllOverlays()", v-on:click="openTabZoom(user, filterSinceDate, filterUntilDate, isGroupMerged(getGroupName(repo)))" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`repo-${i}-author-${j}-commit-breakdown`)", + v-on:mouseout="resetTooltip(`repo-${i}-author-${j}-commit-breakdown`)" + ) font-awesome-icon.icon-button( icon="list-ul", v-bind:class="{ 'active-icon': isSelectedTab(user.name, user.repoName, 'zoom', false) }" ) - span.tooltip-text Click to view breakdown of commits + span.tooltip-text( + v-bind:ref="`repo-${i}-author-${j}-commit-breakdown`" + ) Click to view breakdown of commits a( v-if="isChartGroupWidgetMode", v-bind:href="getReportLink()", target="_blank" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`repo-${i}-author-${j}-commit-breakdown-group-widget`)", + v-on:mouseout="resetTooltip(`repo-${i}-author-${j}-commit-breakdown-group-widget`)" + ) font-awesome-icon.icon-button( icon="arrow-up-right-from-square", ) span.tooltip-text( v-if="!isChartGroupWidgetMode", + v-bind:ref="`repo-${i}-author-${j}-commit-breakdown-group-widget`" ) Click to view breakdown of commits on RepoSense a( v-if="!isChartGroupWidgetMode", v-on:click="getEmbeddedIframe(i , j)" ) - .tooltip(v-bind:id="'tooltip-' + i + '-' + j") + .tooltip( + v-bind:id="'tooltip-' + i + '-' + j", + v-on:mouseover="onTooltipHover(`repo-${i}-author-${j}-iframe-link`)", + v-on:mouseout="resetTooltip(`repo-${i}-author-${j}-iframe-link`)" + ) font-awesome-icon.icon-button(icon="clipboard") - span.tooltip-text Click to copy iframe link + span.tooltip-text(v-bind:ref="`repo-${i}-author-${j}-iframe-link`") Click to copy iframe link .tooltip.summary-chart__title--percentile( v-if="filterGroupSelection === 'groupByNone' && sortGroupSelection.includes('totalCommits')" ) {{ getPercentile(j) }} %  @@ -239,6 +295,7 @@ import { defineComponent } from 'vue'; import { mapState } from 'vuex'; import brokenLinkDisabler from '../mixin/brokenLinkMixin'; +import tooltipPositioner from '../mixin/dynamicTooltipMixin'; import cRamp from './c-ramp.vue'; import cStackedBarChart from './c-stacked-bar-chart.vue'; import { Bar, Repo, User } from '../types/types'; @@ -252,7 +309,7 @@ export default defineComponent({ cRamp, cStackedBarChart, }, - mixins: [brokenLinkDisabler], + mixins: [brokenLinkDisabler, tooltipPositioner], props: { checkedFileTypes: { type: Array, diff --git a/frontend/src/mixin/dynamicTooltipMixin.ts b/frontend/src/mixin/dynamicTooltipMixin.ts new file mode 100644 index 0000000000..5d504c9f8f --- /dev/null +++ b/frontend/src/mixin/dynamicTooltipMixin.ts @@ -0,0 +1,19 @@ +import { defineComponent } from 'vue'; + +export default defineComponent({ + methods: { + onTooltipHover(refName: string): void { + const tooltipTextElement = (this.$refs[refName] as HTMLElement[])[0]; + if (this.isElementAboveViewport(tooltipTextElement)) { + tooltipTextElement.classList.add('bottom-aligned'); + } + }, + resetTooltip(refName: string): void { + const tooltipTextElement = (this.$refs[refName] as HTMLElement[])[0]; + tooltipTextElement.classList.remove('bottom-aligned'); + }, + isElementAboveViewport(el: Element): boolean { + return el.getBoundingClientRect().top <= 0; + }, + }, +}); diff --git a/frontend/src/utils/repo-sorter.js b/frontend/src/utils/repo-sorter.js deleted file mode 100644 index 673fc7d509..0000000000 --- a/frontend/src/utils/repo-sorter.js +++ /dev/null @@ -1,125 +0,0 @@ -function getTotalCommits(total, group) { - return total + group.checkedFileTypeContribution; -} - -function getGroupCommitsVariance(total, group) { - return total + group.variance; -} - -function sortingHelper(element, sortingOption) { - if (sortingOption === 'totalCommits') { - return element.reduce(getTotalCommits, 0); - } - if (sortingOption === 'variance') { - return element.reduce(getGroupCommitsVariance, 0); - } - if (sortingOption === 'displayName') { - return window.getAuthorDisplayName(element); - } - return element[0][sortingOption]; -} - -function groupByRepos(repos, sortingControl) { - const sortedRepos = []; - const { - sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, - } = sortingControl; - const sortWithinOption = sortingWithinOption === 'title' ? 'displayName' : sortingWithinOption; - const sortOption = sortingOption === 'groupTitle' ? 'searchPath' : sortingOption; - repos.forEach((users) => { - if (sortWithinOption === 'totalCommits') { - users.sort(window.comparator((ele) => ele.checkedFileTypeContribution)); - } else { - users.sort(window.comparator((ele) => ele[sortWithinOption])); - } - - if (isSortingWithinDsc) { - users.reverse(); - } - sortedRepos.push(users); - }); - sortedRepos.sort(window.comparator(sortingHelper, sortOption)); - if (isSortingDsc) { - sortedRepos.reverse(); - } - return sortedRepos; -} - -function groupByNone(repos, sortingControl) { - const sortedRepos = []; - const { sortingOption, isSortingDsc } = sortingControl; - const isSortingGroupTitle = sortingOption === 'groupTitle'; - repos.forEach((users) => { - users.forEach((user) => { - sortedRepos.push(user); - }); - }); - sortedRepos.sort(window.comparator((repo) => { - if (isSortingGroupTitle) { - return `${repo.searchPath}${repo.name}`; - } - if (sortingOption === 'totalCommits') { - return repo.checkedFileTypeContribution; - } - return repo[sortingOption]; - })); - if (isSortingDsc) { - sortedRepos.reverse(); - } - - return sortedRepos; -} - -function groupByAuthors(repos, sortingControl) { - const authorMap = {}; - const filtered = []; - const { - sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, - } = sortingControl; - const sortWithinOption = sortingWithinOption === 'title' ? 'searchPath' : sortingWithinOption; - const sortOption = sortingOption === 'groupTitle' ? 'displayName' : sortingOption; - repos.forEach((users) => { - users.forEach((user) => { - if (Object.keys(authorMap).includes(user.name)) { - authorMap[user.name].push(user); - } else { - authorMap[user.name] = [user]; - } - }); - }); - Object.keys(authorMap).forEach((author) => { - if (sortWithinOption === 'totalCommits') { - authorMap[author].sort(window.comparator((repo) => repo.checkedFileTypeContribution)); - } else { - authorMap[author].sort(window.comparator((repo) => repo[sortWithinOption])); - } - if (isSortingWithinDsc) { - authorMap[author].reverse(); - } - filtered.push(authorMap[author]); - }); - - filtered.sort(window.comparator(sortingHelper, sortOption)); - if (isSortingDsc) { - filtered.reverse(); - } - return filtered; -} - -function sortFiltered(filtered, filterControl) { - const { filterGroupSelection } = filterControl; - let full = []; - - if (filterGroupSelection === 'groupByNone') { - // push all repos into the same group - full[0] = groupByNone(filtered, filterControl); - } else if (filterGroupSelection === 'groupByAuthors') { - full = groupByAuthors(filtered, filterControl); - } else { - full = groupByRepos(filtered, filterControl); - } - - return full; -} - -export default sortFiltered; diff --git a/frontend/src/utils/repo-sorter.ts b/frontend/src/utils/repo-sorter.ts new file mode 100644 index 0000000000..7c1fd560d8 --- /dev/null +++ b/frontend/src/utils/repo-sorter.ts @@ -0,0 +1,182 @@ +import { User } from '../types/types'; +import { FilterGroupSelection } from '../types/summary'; + +function getTotalCommits(total: number, group: User): number { + // If group.checkedFileTypeContribution === undefined, then we treat it as 0 contribution + return total + (group.checkedFileTypeContribution ?? 0); +} + +function getGroupCommitsVariance(total: number, group: User): number { + return total + group.variance; +} + +function checkKeyAndGetValue(element: T, key?: string) { + // invalid key provided + if (key === undefined || !(key in element)) { + return undefined; + } + return element[key as keyof T]; +} + +/** + * Returns an empty string if an invalid/no key is provided or if the value retrieved is not a ComparablePrimitive. + * This permits and results in no sorting being done in the above cases. If function is to be made stricter, assertions + * or errors may be thrown where empty strings are returned. + */ +function getComparablePrimitive(element: User, key?: string): string | number { + const val = checkKeyAndGetValue(element, key); + // value retrieved is not a comparable primitive + if (typeof val !== 'string' && typeof val !== 'number') { + return ''; + } + return val; +} + +/** + * Array is not sorted when sortingOption is not provided. sortingOption is optional to allow it to fit the + * SortingFunction interface. + * */ +function sortingHelper(element: User[], sortingOption?: string): string | number { + switch (sortingOption) { + case 'totalCommits': + return element.reduce(getTotalCommits, 0); + case 'variance': + return element.reduce(getGroupCommitsVariance, 0); + case 'displayName': + return window.getAuthorDisplayName(element); + default: + return getComparablePrimitive(element[0], sortingOption); + } +} + +function groupByRepos( + repos: User[][], + sortingControl: { + sortingOption: string; + sortingWithinOption: string; + isSortingDsc: string; + isSortingWithinDsc: string; }, +): User[][] { + const sortedRepos: User[][] = []; + const { + sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, + } = sortingControl; + const sortWithinOption = sortingWithinOption === 'title' ? 'displayName' : sortingWithinOption; + const sortOption = sortingOption === 'groupTitle' ? 'searchPath' : sortingOption; + repos.forEach((users) => { + if (sortWithinOption === 'totalCommits') { + users.sort(window.comparator((ele) => ele.checkedFileTypeContribution ?? 0)); + } else { + users.sort(window.comparator((ele) => getComparablePrimitive(ele, sortWithinOption))); + } + + if (isSortingWithinDsc) { + users.reverse(); + } + sortedRepos.push(users); + }); + sortedRepos.sort(window.comparator(sortingHelper, sortOption)); + if (isSortingDsc) { + sortedRepos.reverse(); + } + return sortedRepos; +} + +function groupByNone( + repos: User[][], + sortingControl: { + sortingOption: string; + isSortingDsc: string; }, +): User[] { + const sortedRepos: User[] = []; + const { sortingOption, isSortingDsc } = sortingControl; + const isSortingGroupTitle = sortingOption === 'groupTitle'; + repos.forEach((users) => { + users.forEach((user) => { + sortedRepos.push(user); + }); + }); + sortedRepos.sort(window.comparator((repo) => { + if (isSortingGroupTitle) { + return `${repo.searchPath}${repo.name}`; + } + if (sortingOption === 'totalCommits') { + return repo.checkedFileTypeContribution ?? 0; + } + return getComparablePrimitive(repo, sortingOption); + })); + if (isSortingDsc) { + sortedRepos.reverse(); + } + + return sortedRepos; +} + +function groupByAuthors( + repos: User[][], + sortingControl: { + sortingOption: string; + sortingWithinOption: string; + isSortingDsc: string; + isSortingWithinDsc: string; }, +): User[][] { + const authorMap: { [userName: string]: User[] } = {}; + const filtered: User[][] = []; + const { + sortingWithinOption, sortingOption, isSortingDsc, isSortingWithinDsc, + } = sortingControl; + const sortWithinOption = sortingWithinOption === 'title' ? 'searchPath' : sortingWithinOption; + const sortOption = sortingOption === 'groupTitle' ? 'displayName' : sortingOption; + repos.forEach((users) => { + users.forEach((user) => { + if (Object.keys(authorMap).includes(user.name)) { + authorMap[user.name].push(user); + } else { + authorMap[user.name] = [user]; + } + }); + }); + Object.keys(authorMap).forEach((author) => { + if (sortWithinOption === 'totalCommits') { + authorMap[author].sort(window.comparator((repo) => repo.checkedFileTypeContribution ?? 0)); + } else { + authorMap[author].sort(window.comparator((repo) => getComparablePrimitive(repo, sortingWithinOption))); + } + if (isSortingWithinDsc) { + authorMap[author].reverse(); + } + filtered.push(authorMap[author]); + }); + + filtered.sort(window.comparator(sortingHelper, sortOption)); + if (isSortingDsc) { + filtered.reverse(); + } + return filtered; +} + +function sortFiltered( + filtered: User[][], + filterControl: { + filterGroupSelection: FilterGroupSelection; + sortingOption: string; + sortingWithinOption: string; + isSortingDsc: string; + isSortingWithinDsc: string; }, +): User[][] { + const { filterGroupSelection } = filterControl; + let full = []; + + if (filterGroupSelection === 'groupByNone') { + // push all repos into the same group + full[0] = groupByNone(filtered, filterControl); + } else if (filterGroupSelection === 'groupByAuthors') { + full = groupByAuthors(filtered, filterControl); + } else { + full = groupByRepos(filtered, filterControl); + } + + return full; +} + +export default sortFiltered; diff --git a/frontend/src/utils/safari_date.js b/frontend/src/utils/safari_date.js deleted file mode 100644 index 117353ed11..0000000000 --- a/frontend/src/utils/safari_date.js +++ /dev/null @@ -1,49 +0,0 @@ -// date keys for handling safari date input // -function isIntegerKey(key) { - return (key >= 48 && key <= 57) || (key >= 96 && key <= 105); -} - -function isArrowOrEnterKey(key) { - return (key >= 37 && key <= 40) || key === 13; -} - -function isBackSpaceOrDeleteKey(key) { - return key === 8 || key === 46; -} - -function validateInputDate(event) { - const key = event.keyCode; - // only allow integer, backspace, delete, arrow or enter keys - if (!(isIntegerKey(key) || isBackSpaceOrDeleteKey(key) || isArrowOrEnterKey(key))) { - event.preventDefault(); - } -} - -function deleteDashInputDate(event) { - const key = event.keyCode; - const date = event.target.value; - // remove two chars before the cursor's position if deleting dash character - if (isBackSpaceOrDeleteKey(key)) { - const cursorPosition = event.target.selectionStart; - if (date[cursorPosition - 1] === '-') { - event.target.value = date.slice(0, cursorPosition - 1); - } - } -} - -window.formatInputDateOnKeyDown = function formatInputDateOnKeyDown(event) { - validateInputDate(event); - deleteDashInputDate(event); -}; - -window.appendDashInputDate = function appendDashInputDate(event) { - const date = event.target.value; - // append dash to date with format yyyy-mm-dd - if (date.match(/^\d{4}$/) !== null) { - event.target.value = `${event.target.value}-`; - } else if (date.match(/^\d{4}-\d{2}$/) !== null) { - event.target.value = `${event.target.value}-`; - } -}; - -export default 'test'; diff --git a/frontend/src/utils/safari_date.ts b/frontend/src/utils/safari_date.ts new file mode 100644 index 0000000000..a670453aa9 --- /dev/null +++ b/frontend/src/utils/safari_date.ts @@ -0,0 +1,54 @@ +// date keys for handling safari date input // +function isIntegerKey(key: string) { + return !Number.isNaN(+key); +} + +function isArrowOrEnterKey(key: string) { + return key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight' || key === 'ArrowUp' || key === 'Enter'; +} + +function isBackSpaceOrDeleteKey(key: string) { + return key === 'Backspace' || key === 'Delete'; +} + +function validateInputDate(event: KeyboardEvent) { + const key = event.key; + // only allow integer, backspace, delete, arrow or enter keys + if (!(isIntegerKey(key) || isBackSpaceOrDeleteKey(key) || isArrowOrEnterKey(key))) { + event.preventDefault(); + } +} + +function deleteDashInputDate(event: KeyboardEvent) { + const key = event.key; + // remove two chars before the cursor's position if deleting dash character + if (isBackSpaceOrDeleteKey(key) && event.target !== null && 'value' in event.target + && 'selectionStart' in event.target) { + const date = event.target.value as string; + const cursorPosition = event.target.selectionStart as number; + if (date[cursorPosition - 1] === '-') { + event.target.value = date.slice(0, cursorPosition - 1); + } + } +} + +function formatInputDateOnKeyDown(event: KeyboardEvent) { + validateInputDate(event); + deleteDashInputDate(event); +} + +function appendDashInputDate(event: KeyboardEvent) { + // append dash to date with format yyyy-mm-dd + if (event.target !== null && 'value' in event.target) { + const date = event.target.value as string; + if (date.match(/^\d{4}$/) !== null) { + event.target.value = `${event.target.value}-`; + } else if (date.match(/^\d{4}-\d{2}$/) !== null) { + event.target.value = `${event.target.value}-`; + } + } +} + +Object.assign(window, { formatInputDateOnKeyDown, appendDashInputDate }); + +export default 'test'; diff --git a/frontend/src/views/c-authorship.vue b/frontend/src/views/c-authorship.vue index fd6039059a..a4ac53aae3 100644 --- a/frontend/src/views/c-authorship.vue +++ b/frontend/src/views/c-authorship.vue @@ -148,18 +148,28 @@ v-bind:class="!isBrokenLink(getHistoryLink(file)) ? '' : 'broken-link'", v-bind:href="getHistoryLink(file)", target="_blank" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`${file.path}-view-history-tooltip`)", + v-on:mouseout="resetTooltip(`${file.path}-view-history-tooltip`)" + ) font-awesome-icon.button(icon="history") - span.tooltip-text {{getLinkMessage(getHistoryLink(file), 'Click to view the history view of file')}} + span.tooltip-text( + v-bind:ref="`${file.path}-view-history-tooltip`" + ) {{getLinkMessage(getHistoryLink(file), 'Click to view the history view of file')}} a( v-if='!file.isBinary', v-bind:class="!isBrokenLink(getBlameLink(file)) ? '' : 'broken-link'", v-bind:href="getBlameLink(file)", target="_blank", title="click to view the blame view of file" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`${file.path}-view-blame-tooltip`)", + v-on:mouseout="resetTooltip(`${file.path}-view-blame-tooltip`)" + ) font-awesome-icon.button(icon="user-edit") - span.tooltip-text {{getLinkMessage(getBlameLink(file), 'Click to view the blame view of file')}} + span.tooltip-text( + v-bind:ref="`${file.path}-view-blame-tooltip`" + ) {{getLinkMessage(getBlameLink(file), 'Click to view the blame view of file')}} .author-breakdown(v-if="info.isMergeGroup") .author-breakdown__legend( v-for="author in getAuthors(file)", @@ -186,6 +196,7 @@ import { defineComponent } from 'vue'; import { mapState } from 'vuex'; import minimatch from 'minimatch'; import brokenLinkDisabler from '../mixin/brokenLinkMixin'; +import tooltipPositioner from '../mixin/dynamicTooltipMixin'; import cSegmentCollection from '../components/c-segment-collection.vue'; import Segment from '../utils/segment'; import getNonRepeatingColor from '../utils/random-color-generator'; @@ -229,7 +240,7 @@ export default defineComponent({ components: { cSegmentCollection, }, - mixins: [brokenLinkDisabler], + mixins: [brokenLinkDisabler, tooltipPositioner], emits: [ 'deactivate-tab', ], @@ -488,22 +499,6 @@ export default defineComponent({ } }, - onTooltipHover(refName: string): void { - const tooltipTextElement = (this.$refs[refName] as HTMLElement[])[0]; - if (this.isElementAboveViewport(tooltipTextElement)) { - tooltipTextElement.classList.add('bottom-aligned'); - } - }, - - resetTooltip(refName: string): void { - const tooltipTextElement = (this.$refs[refName] as HTMLElement[])[0]; - tooltipTextElement.classList.remove('bottom-aligned'); - }, - - isElementAboveViewport(el: Element): boolean { - return el.getBoundingClientRect().top <= 0; - }, - isUnknownAuthor(name: string): boolean { return name === '-'; }, diff --git a/frontend/src/views/c-zoom.vue b/frontend/src/views/c-zoom.vue index ec8ee458c0..08509740b4 100644 --- a/frontend/src/views/c-zoom.vue +++ b/frontend/src/views/c-zoom.vue @@ -126,9 +126,14 @@ v-if="slice.messageBody !== ''", v-on:click="toggleSelectedCommitMessageBody(slice)" ) - .tooltip + .tooltip( + v-on:mouseover="onTooltipHover(`${slice.hash}-show-hide-message-body`)", + v-on:mouseout="resetTooltip(`${slice.hash}-show-hide-message-body`)" + ) font-awesome-icon.commit-message--button(icon="ellipsis-h") - span.tooltip-text Click to show/hide the commit message body + span.tooltip-text( + v-bind:ref="`${slice.hash}-show-hide-message-body`" + ) Click to show/hide the commit message body .body(v-if="slice.messageBody !== ''", v-show="slice.isOpen") pre {{ slice.messageBody }} .dashed-border @@ -145,6 +150,7 @@ import { defineComponent } from 'vue'; import { mapState } from 'vuex'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import brokenLinkDisabler from '../mixin/brokenLinkMixin'; +import tooltipPositioner from '../mixin/dynamicTooltipMixin'; import cRamp from '../components/c-ramp.vue'; import cStackedBarChart from '../components/c-stacked-bar-chart.vue'; import User from '../utils/user'; @@ -177,7 +183,7 @@ export default defineComponent({ cRamp, cStackedBarChart, }, - mixins: [brokenLinkDisabler], + mixins: [brokenLinkDisabler, tooltipPositioner], data() { return { ...zoomInitialState(),