diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-28 09:09:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-28 09:09:06 +0000 |
commit | 7e8278c0f46cf6058efad5afd0aef177977bd663 (patch) | |
tree | 7ac46710921145bb782bcb208ea896e1548b168b /app/assets | |
parent | bbf6581214128ae12a6ff32f66a0d03ee57a2e91 (diff) | |
download | gitlab-ce-7e8278c0f46cf6058efad5afd0aef177977bd663.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r-- | app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js | 47 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 36 | ||||
-rw-r--r-- | app/assets/javascripts/pages/projects/init_blob.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/repository/utils/title.js | 14 | ||||
-rw-r--r-- | app/assets/stylesheets/framework.scss | 1 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/carousel.scss | 202 |
6 files changed, 297 insertions, 4 deletions
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index 052e33b4a2b..d5d8edd5ac0 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -1,26 +1,67 @@ import Mousetrap from 'mousetrap'; -import { getLocationHash, visitUrl } from '../../lib/utils/url_utility'; +import { + getLocationHash, + updateHistory, + urlIsDifferent, + urlContainsSha, + getShaFromUrl, +} from '~/lib/utils/url_utility'; +import { updateRefPortionOfTitle } from '~/repository/utils/title'; import Shortcuts from './shortcuts'; const defaults = { skipResetBindings: false, fileBlobPermalinkUrl: null, + fileBlobPermalinkUrlElement: null, }; +function eventHasModifierKeys(event) { + // We ignore alt because I don't think alt clicks normally do anything special? + return event.ctrlKey || event.metaKey || event.shiftKey; +} + export default class ShortcutsBlob extends Shortcuts { constructor(opts) { const options = Object.assign({}, defaults, opts); super(options.skipResetBindings); this.options = options; + this.shortcircuitPermalinkButton(); + Mousetrap.bind('y', this.moveToFilePermalink.bind(this)); } moveToFilePermalink() { - if (this.options.fileBlobPermalinkUrl) { + const permalink = this.options.fileBlobPermalinkUrl; + + if (permalink) { const hash = getLocationHash(); const hashUrlString = hash ? `#${hash}` : ''; - visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`); + + if (urlIsDifferent(permalink)) { + updateHistory({ + url: `${permalink}${hashUrlString}`, + title: document.title, + }); + } + + if (urlContainsSha({ url: permalink })) { + updateRefPortionOfTitle(getShaFromUrl({ url: permalink })); + } + } + } + + shortcircuitPermalinkButton() { + const button = this.options.fileBlobPermalinkUrlElement; + const handleButton = e => { + if (!eventHasModifierKeys(e)) { + e.preventDefault(); + this.moveToFilePermalink(); + } + }; + + if (button) { + button.addEventListener('click', handleButton); } } } diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index d48678c21f6..202363a1dda 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,6 +1,14 @@ const PATH_SEPARATOR = '/'; const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`); const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`); +const SHA_REGEX = /[\da-f]{40}/gi; + +// Reset the cursor in a Regex so that multiple uses before a recompile don't fail +function resetRegExp(regex) { + regex.lastIndex = 0; /* eslint-disable-line no-param-reassign */ + + return regex; +} // Returns a decoded url parameter value // - Treats '+' as '%20' @@ -128,6 +136,20 @@ export function doesHashExistInUrl(hashName) { return hash && hash.includes(hashName); } +export function urlContainsSha({ url = String(window.location) } = {}) { + return resetRegExp(SHA_REGEX).test(url); +} + +export function getShaFromUrl({ url = String(window.location) } = {}) { + let sha = null; + + if (urlContainsSha({ url })) { + [sha] = url.match(resetRegExp(SHA_REGEX)); + } + + return sha; +} + /** * Apply the fragment to the given url by returning a new url string that includes * the fragment. If the given url already contains a fragment, the original fragment @@ -154,6 +176,16 @@ export function visitUrl(url, external = false) { } } +export function updateHistory({ state = {}, title = '', url, replace = false, win = window } = {}) { + if (win.history) { + if (replace) { + win.history.replaceState(state, title, url); + } else { + win.history.pushState(state, title, url); + } + } +} + export function refreshCurrentPage() { visitUrl(window.location.href); } @@ -282,3 +314,7 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f }; export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); + +export function urlIsDifferent(url, compare = String(window.location)) { + return url !== compare; +} diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index bd8afa2d5ba..e862456f429 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -25,6 +25,7 @@ export default () => { new ShortcutsBlob({ skipResetBindings: true, fileBlobPermalinkUrl, + fileBlobPermalinkUrlElement, }); new BlobForkSuggestion({ diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js index ff16fbdd420..9c4b334a1ce 100644 --- a/app/assets/javascripts/repository/utils/title.js +++ b/app/assets/javascripts/repository/utils/title.js @@ -1,5 +1,5 @@ const DEFAULT_TITLE = '· GitLab'; -// eslint-disable-next-line import/prefer-default-export + export const setTitle = (pathMatch, ref, project) => { if (!pathMatch) { document.title = `${project} ${DEFAULT_TITLE}`; @@ -12,3 +12,15 @@ export const setTitle = (pathMatch, ref, project) => { /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project} ${DEFAULT_TITLE}`; }; + +export function updateRefPortionOfTitle(sha, doc = document) { + const { title = '' } = doc; + const titleParts = title.split(' · '); + + if (titleParts.length > 1) { + titleParts[1] = sha; + + /* eslint-disable-next-line no-param-reassign */ + doc.title = titleParts.join(' · '); + } +} diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 249e9a24b17..9032dd28b80 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -15,6 +15,7 @@ @import 'framework/badges'; @import 'framework/calendar'; @import 'framework/callout'; +@import 'framework/carousel'; @import 'framework/common'; @import 'framework/dropdowns'; @import 'framework/files'; diff --git a/app/assets/stylesheets/framework/carousel.scss b/app/assets/stylesheets/framework/carousel.scss new file mode 100644 index 00000000000..d51a9f9c173 --- /dev/null +++ b/app/assets/stylesheets/framework/carousel.scss @@ -0,0 +1,202 @@ +// Notes on the classes: +// +// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically) +// even when their scroll action started on a carousel, but for compatibility (with Firefox) +// we're preventing all actions instead +// 2. The .carousel-item-left and .carousel-item-right is used to indicate where +// the active slide is heading. +// 3. .active.carousel-item is the current slide. +// 4. .active.carousel-item-left and .active.carousel-item-right is the current +// slide in its in-transition state. Only one of these occurs at a time. +// 5. .carousel-item-next.carousel-item-left and .carousel-item-prev.carousel-item-right +// is the upcoming slide in transition. + +.carousel { + position: relative; + + &.pointer-event { + touch-action: pan-y; + } +} + + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; + @include clearfix(); +} + +.carousel-item { + position: relative; + display: none; + float: left; + width: 100%; + margin-right: -100%; + backface-visibility: hidden; + @include transition($carousel-transition); +} + +.carousel-item.active, +.carousel-item-next, +.carousel-item-prev { + display: block; +} + +.carousel-item-next:not(.carousel-item-left), +.active.carousel-item-right { + transform: translateX(100%); +} + +.carousel-item-prev:not(.carousel-item-right), +.active.carousel-item-left { + transform: translateX(-100%); +} + + +// +// Alternate transitions +// + +.carousel-fade { + .carousel-item { + opacity: 0; + transition-property: opacity; + transform: none; + } + + .carousel-item.active, + .carousel-item-next.carousel-item-left, + .carousel-item-prev.carousel-item-right { + z-index: 1; + opacity: 1; + } + + .active.carousel-item-left, + .active.carousel-item-right { + z-index: 0; + opacity: 0; + @include transition(0s $carousel-transition-duration opacity); + } +} + + +// +// Left/right controls for nav +// + +.carousel-control-prev, +.carousel-control-next { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + // Use flex for alignment (1-3) + display: flex; // 1. allow flex styles + align-items: center; // 2. vertically center contents + justify-content: center; // 3. horizontally center contents + width: $carousel-control-width; + color: $carousel-control-color; + text-align: center; + opacity: $carousel-control-opacity; + @include transition($carousel-control-transition); + + // Hover/focus state + @include hover-focus { + color: $carousel-control-color; + text-decoration: none; + outline: 0; + opacity: $carousel-control-hover-opacity; + } +} + +.carousel-control-prev { + left: 0; + @if $enable-gradients { + background: linear-gradient(90deg, rgba($black, 0.25), rgba($black, 0.001)); + } +} + +.carousel-control-next { + right: 0; + @if $enable-gradients { + background: linear-gradient(270deg, rgba($black, 0.25), rgba($black, 0.001)); + } +} + +// Icons for within +.carousel-control-prev-icon, +.carousel-control-next-icon { + display: inline-block; + width: $carousel-control-icon-width; + height: $carousel-control-icon-width; + background: no-repeat 50% / 100% 100%; +} + +.carousel-control-prev-icon { + background-image: $carousel-control-prev-icon-bg; +} + +.carousel-control-next-icon { + background-image: $carousel-control-next-icon-bg; +} + + +// Optional indicator pips +// +// Add an ordered list with the following class and add a list item for each +// slide your carousel holds. + +.carousel-indicators { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 15; + display: flex; + justify-content: center; + padding-left: 0; // override <ol> default + // Use the .carousel-control's width as margin so we don't overlay those + margin-right: $carousel-control-width; + margin-left: $carousel-control-width; + list-style: none; + + li { + box-sizing: content-box; + flex: 0 1 auto; + width: $carousel-indicator-width; + height: $carousel-indicator-height; + margin-right: $carousel-indicator-spacer; + margin-left: $carousel-indicator-spacer; + text-indent: -999px; + cursor: pointer; + background-color: $carousel-indicator-active-bg; + background-clip: padding-box; + // Use transparent borders to increase the hit area by 10px on top and bottom. + border-top: $carousel-indicator-hit-area-height solid transparent; + border-bottom: $carousel-indicator-hit-area-height solid transparent; + opacity: 0.5; + @include transition($carousel-indicator-transition); + } + + .active { + opacity: 1; + } +} + + +// Optional captions +// +// + +.carousel-caption { + position: absolute; + right: (100% - $carousel-caption-width) / 2; + bottom: 20px; + left: (100% - $carousel-caption-width) / 2; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: $carousel-caption-color; + text-align: center; +} |