diff options
Diffstat (limited to 'app/assets/javascripts')
19 files changed, 759 insertions, 28 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index 1325fc993b2..3d59410cbc2 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -43,6 +43,15 @@ export default { required: false, default: false, }, + activeFileKey: { + type: String, + required: false, + default: null, + }, + keyPrefix: { + type: String, + required: true, + }, }, data() { return { @@ -113,8 +122,9 @@ export default { <list-item :file="file" :action-component="itemActionComponent" - :key-prefix="title" + :key-prefix="keyPrefix" :staged-list="stagedList" + :active-file-key="activeFileKey" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 03f3e4de83c..6c30b2a721d 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -30,6 +30,11 @@ export default { required: false, default: false, }, + activeFileKey: { + type: String, + required: false, + default: null, + }, }, computed: { iconName() { @@ -39,6 +44,12 @@ export default { iconClass() { return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; }, + fullKey() { + return `${this.keyPrefix}-${this.file.key}`; + }, + isActive() { + return this.activeFileKey === this.fullKey; + }, }, methods: { ...mapActions([ @@ -51,7 +62,7 @@ export default { openFileInEditor() { return this.openPendingTab({ file: this.file, - keyPrefix: this.keyPrefix.toLowerCase(), + keyPrefix: this.keyPrefix, }).then(changeViewer => { if (changeViewer) { this.updateViewer(viewerTypes.diff); @@ -70,7 +81,12 @@ export default { </script> <template> - <div class="multi-file-commit-list-item"> + <div + class="multi-file-commit-list-item" + :class="{ + 'is-active': isActive + }" + > <button type="button" class="multi-file-commit-list-path" diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 0b696596f77..7f1ac63d543 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import CommitFilesList from './commit_sidebar/list.vue'; import EmptyState from './commit_sidebar/empty_state.vue'; import * as consts from '../stores/modules/commit/constants'; -import { activityBarViews } from '../constants'; +import { activityBarViews, stageKeys } from '../constants'; export default { components: { @@ -27,11 +27,14 @@ export default { 'unusedSeal', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), showStageUnstageArea() { return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); }, + activeFileKey() { + return this.activeFile ? this.activeFile.key : null; + }, }, watch: { hasChanges() { @@ -44,6 +47,7 @@ export default { if (this.lastOpenedFile) { this.openPendingTab({ file: this.lastOpenedFile, + keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, }) .then(changeViewer => { if (changeViewer) { @@ -62,6 +66,7 @@ export default { return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); }, }, + stageKeys, }; </script> @@ -86,21 +91,25 @@ export default { > <commit-files-list :title="__('Unstaged')" + :key-prefix="$options.stageKeys.unstaged" :file-list="changedFiles" :action-btn-text="__('Stage all')" class="is-first" icon-name="unstaged" action="stageAllChanges" item-action-component="stage-button" + :active-file-key="activeFileKey" /> <commit-files-list :title="__('Staged')" + :key-prefix="$options.stageKeys.staged" :file-list="stagedFiles" :action-btn-text="__('Unstage all')" :staged-list="true" icon-name="staged" action="unstageAllChanges" item-action-component="unstage-button" + :active-file-key="activeFileKey" /> </template> <empty-state diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index a726eae3a96..24b6a4fdea1 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -2,6 +2,7 @@ import { mapState, mapGetters, mapActions } from 'vuex'; import flash from '~/flash'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; +import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import { activityBarViews, viewerTypes } from '../constants'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; @@ -9,6 +10,7 @@ import ExternalLink from './external_link.vue'; export default { components: { ContentViewer, + DiffViewer, ExternalLink, }, props: { @@ -29,9 +31,18 @@ export default { shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, + showContentViewer() { + return ( + (this.shouldHideEditor || this.file.viewMode === 'preview') && + (this.viewer !== viewerTypes.mr || !this.file.mrChange) + ); + }, + showDiffViewer() { + return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr; + }, editTabCSS() { return { - active: this.file.viewMode === 'edit', + active: this.file.viewMode === 'editor', }; }, previewTabCSS() { @@ -53,7 +64,7 @@ export default { if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'edit', + viewMode: 'editor', }); } } @@ -62,7 +73,7 @@ export default { if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'edit', + viewMode: 'editor', }); } }, @@ -197,7 +208,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> + @click.prevent="setFileViewMode({ file, viewMode: 'editor' })"> <template v-if="viewer === $options.viewerTypes.edit"> {{ __('Edit') }} </template> @@ -222,7 +233,7 @@ export default { /> </div> <div - v-show="!shouldHideEditor && file.viewMode === 'edit'" + v-show="!shouldHideEditor && file.viewMode ==='editor'" ref="editor" :class="{ 'is-readonly': isCommitModeActive, @@ -231,10 +242,18 @@ export default { > </div> <content-viewer - v-if="shouldHideEditor || file.viewMode === 'preview'" + v-if="showContentViewer" :content="file.content || file.raw" :path="file.rawPath || file.path" :file-size="file.size" :project-path="file.projectId"/> + <diff-viewer + v-if="showDiffViewer" + :diff-mode="file.mrChange.diffMode" + :new-path="file.mrChange.new_path" + :new-sha="currentMergeRequest.sha" + :old-path="file.mrChange.old_path" + :old-sha="currentMergeRequest.baseCommitSha" + :project-path="file.projectId"/> </div> </template> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 65886c02b92..12e0c3aeef0 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -21,7 +21,19 @@ export const viewerTypes = { diff: 'diff', }; +export const diffModes = { + replaced: 'replaced', + new: 'new', + deleted: 'deleted', + renamed: 'renamed', +}; + export const rightSidebarViews = { pipelines: 'pipelines-list', jobsDetail: 'jobs-detail', }; + +export const stageKeys = { + unstaged: 'unstaged', + staged: 'staged', +}; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 13f123b6630..5826f6cb828 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; +import { diffModes } from '../../constants'; export default { [types.SET_FILE_ACTIVE](state, { path, active }) { @@ -85,8 +86,19 @@ export default { }); }, [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) { + let diffMode = diffModes.replaced; + if (mrChange.new_file) { + diffMode = diffModes.new; + } else if (mrChange.deleted_file) { + diffMode = diffModes.deleted; + } else if (mrChange.renamed_file) { + diffMode = diffModes.renamed; + } Object.assign(state.entries[file.path], { - mrChange, + mrChange: { + ...mrChange, + diffMode, + }, }); }, [types.SET_FILE_VIEWMODE](state, { file, viewMode }) { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index e0b9766fbee..a04a33cd12d 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -39,7 +39,7 @@ export const dataStructure = () => ({ editorColumn: 1, fileLanguage: '', eol: '', - viewMode: 'edit', + viewMode: 'editor', previewMode: null, size: 0, parentPath: null, diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index f8b3d3061f0..d269c45203a 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -56,7 +56,7 @@ export default class MilestoneSelect { if (issueUpdateURL) { milestoneLinkTemplate = _.template( - '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', + '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', ); milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; } diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 825de01b5a2..87213c94eda 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -62,13 +62,13 @@ export default class UsernameValidator { return this.setPendingState(); } - if (!this.state.available) { - return this.setUnavailableState(); - } - if (!this.state.valid) { return this.setInvalidState(); } + + if (!this.state.available) { + return this.setUnavailableState(); + } } interceptInvalid(event) { @@ -89,7 +89,6 @@ export default class UsernameValidator { setAvailabilityState(usernameTaken) { if (usernameTaken) { - this.state.valid = false; this.state.available = false; } else { this.state.available = true; diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index f9fda5356e6..f1ef50d0e3d 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -32,7 +32,10 @@ export default { <div class="file-container"> <div class="file-content"> <p class="prepend-top-10 file-info"> - {{ fileName }} ({{ fileSizeReadable }}) + {{ fileName }} + <template v-if="fileSize > 0"> + ({{ fileSizeReadable }}) + </template> </p> <a :href="path" diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index a5999f909ca..6851029018a 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { @@ -12,6 +13,10 @@ export default { required: false, default: 0, }, + renderInfo: { + type: Boolean, + default: true, + }, }, data() { return { @@ -26,14 +31,34 @@ export default { return numberToHumanSize(this.fileSize); }, }, + beforeDestroy() { + window.removeEventListener('resize', this.resizeThrottled, false); + }, + mounted() { + // The onImgLoad may have happened before the control was actually mounted + this.onImgLoad(); + this.resizeThrottled = _.throttle(this.onImgLoad, 400); + window.addEventListener('resize', this.resizeThrottled, false); + }, methods: { onImgLoad() { const contentImg = this.$refs.contentImg; - this.isZoomable = - contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height; - this.width = contentImg.naturalWidth; - this.height = contentImg.naturalHeight; + if (contentImg) { + this.isZoomable = + contentImg.naturalWidth > contentImg.width || + contentImg.naturalHeight > contentImg.height; + + this.width = contentImg.naturalWidth; + this.height = contentImg.naturalHeight; + + this.$emit('imgLoaded', { + width: this.width, + height: this.height, + renderedWidth: contentImg.clientWidth, + renderedHeight: contentImg.clientHeight, + }); + } }, onImgClick() { if (this.isZoomable) this.isZoomed = !this.isZoomed; @@ -47,20 +72,22 @@ export default { <div class="file-content image_file"> <img ref="contentImg" - :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }" + :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }" :src="path" :alt="path" @load="onImgLoad" @click="onImgClick"/> - <p class="file-info prepend-top-10"> + <p + v-if="renderInfo" + class="file-info prepend-top-10"> <template v-if="fileSize>0"> {{ fileSizeReadable }} </template> <template v-if="fileSize>0 && width && height"> - - + | </template> <template v-if="width && height"> - {{ width }} x {{ height }} + W: {{ width }} | H: {{ height }} </template> </p> </div> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js new file mode 100644 index 00000000000..6c1840361af --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js @@ -0,0 +1,12 @@ +export const diffModes = { + replaced: 'replaced', + new: 'new', + deleted: 'deleted', + renamed: 'renamed', +}; + +export const imageViewMode = { + twoup: 'twoup', + swipe: 'swipe', + onion: 'onion', +}; diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue new file mode 100644 index 00000000000..4eca3fd4e97 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -0,0 +1,70 @@ +<script> +import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils'; +import ImageDiffViewer from './viewers/image_diff_viewer.vue'; +import DownloadDiffViewer from './viewers/download_diff_viewer.vue'; + +export default { + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + newSha: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + oldSha: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + viewer() { + if (!this.newPath) return null; + + const previewInfo = viewerInformationForPath(this.newPath); + if (!previewInfo) return DownloadDiffViewer; + + switch (previewInfo.id) { + case 'image': + return ImageDiffViewer; + default: + return DownloadDiffViewer; + } + }, + fullOldPath() { + return `${gon.relative_url_root}/${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`; + }, + fullNewPath() { + return `${gon.relative_url_root}/${this.projectPath}/raw/${this.newSha}/${this.newPath}`; + }, + }, +}; +</script> + +<template> + <div + class="diff-file preview-container" + v-if="viewer"> + <component + :is="viewer" + :diff-mode="diffMode" + :new-path="fullNewPath" + :old-path="fullOldPath" + :project-path="projectPath" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue new file mode 100644 index 00000000000..50389b6ae63 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue @@ -0,0 +1,69 @@ +<script> +import DownloadViewer from '../../content_viewer/viewers/download_viewer.vue'; +import { diffModes } from '../constants'; + +export default { + components: { + DownloadViewer, + }, + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + diffModes, +}; +</script> + +<template> + <div class="diff-file-container"> + <div class="diff-viewer"> + <div + v-if="diffMode === $options.diffModes.replaced" + class="two-up view row"> + <div class="col-sm-6 deleted"> + <download-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + <div class="col-sm-6 added"> + <download-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + </div> + <div + v-else-if="diffMode === $options.diffModes.new" + class="added"> + <download-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + <div + v-else + class="deleted"> + <download-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue new file mode 100644 index 00000000000..efcc39197b0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue @@ -0,0 +1,160 @@ +<script> +import { pixeliseValue } from '../../../lib/utils/dom_utils'; +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + onionMaxWidth: undefined, + onionMaxHeight: undefined, + onionOldImgInfo: null, + onionNewImgInfo: null, + onionDraggerPos: 0, + onionOpacity: 1, + dragging: false, + }; + }, + computed: { + onionMaxPixelWidth() { + return pixeliseValue(this.onionMaxWidth); + }, + onionMaxPixelHeight() { + return pixeliseValue(this.onionMaxHeight); + }, + onionDraggerPixelPos() { + return pixeliseValue(this.onionDraggerPos); + }, + }, + beforeDestroy() { + document.body.removeEventListener('mouseup', this.stopDrag); + this.$refs.dragger.removeEventListener('mousedown', this.startDrag); + }, + methods: { + dragMove(e) { + if (!this.dragging) return; + const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left; + const dragTrackWidth = + this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; + + let leftValue = left; + if (leftValue < 0) leftValue = 0; + if (leftValue > dragTrackWidth) leftValue = dragTrackWidth; + + this.onionOpacity = left / dragTrackWidth; + this.onionDraggerPos = leftValue; + }, + startDrag() { + this.dragging = true; + document.body.style.userSelect = 'none'; + document.body.addEventListener('mousemove', this.dragMove); + }, + stopDrag() { + this.dragging = false; + document.body.style.userSelect = ''; + document.body.removeEventListener('mousemove', this.dragMove); + }, + prepareOnionSkin() { + if (this.onionOldImgInfo && this.onionNewImgInfo) { + this.onionMaxWidth = Math.max( + this.onionOldImgInfo.renderedWidth, + this.onionNewImgInfo.renderedWidth, + ); + this.onionMaxHeight = Math.max( + this.onionOldImgInfo.renderedHeight, + this.onionNewImgInfo.renderedHeight, + ); + + this.onionOpacity = 1; + this.onionDraggerPos = + this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; + + document.body.addEventListener('mouseup', this.stopDrag); + } + }, + onionNewImgLoaded(imgInfo) { + this.onionNewImgInfo = imgInfo; + this.prepareOnionSkin(); + }, + onionOldImgLoaded(imgInfo) { + this.onionOldImgInfo = imgInfo; + this.prepareOnionSkin(); + }, + }, +}; +</script> + +<template> + <div class="onion-skin view"> + <div + class="onion-skin-frame" + :style="{ + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + 'user-select': dragging === true ? 'none' : '', + }"> + <div + class="frame deleted" + :style="{ + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + }"> + <image-viewer + key="onionOldImg" + :render-info="false" + :path="oldPath" + :project-path="projectPath" + @imgLoaded="onionOldImgLoaded" + /> + </div> + <div + class="added frame" + ref="addedFrame" + :style="{ + 'opacity': onionOpacity, + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + }"> + <image-viewer + key="onionNewImg" + :render-info="false" + :path="newPath" + :project-path="projectPath" + @imgLoaded="onionNewImgLoaded" + /> + </div> + <div class="controls"> + <div class="transparent"></div> + <div + class="drag-track" + ref="dragTrack" + @mousedown="startDrag" + @mouseup="stopDrag"> + <div + class="dragger" + ref="dragger" + :style="{ 'left': onionDraggerPixelPos }"> + </div> + </div> + <div class="opaque"></div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue new file mode 100644 index 00000000000..fc513ebfce1 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue @@ -0,0 +1,158 @@ +<script> +import _ from 'underscore'; +import { pixeliseValue } from '../../../lib/utils/dom_utils'; +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + dragging: false, + swipeOldImgInfo: null, + swipeNewImgInfo: null, + swipeMaxWidth: undefined, + swipeMaxHeight: undefined, + swipeBarPos: 1, + swipeWrapWidth: undefined, + }; + }, + computed: { + swipeMaxPixelWidth() { + return pixeliseValue(this.swipeMaxWidth); + }, + swipeMaxPixelHeight() { + return pixeliseValue(this.swipeMaxHeight); + }, + swipeWrapPixelWidth() { + return pixeliseValue(this.swipeWrapWidth); + }, + swipeBarPixelPos() { + return pixeliseValue(this.swipeBarPos); + }, + }, + beforeDestroy() { + window.removeEventListener('resize', this.resizeThrottled, false); + document.body.removeEventListener('mouseup', this.stopDrag); + document.body.removeEventListener('mousemove', this.dragMove); + }, + mounted() { + window.addEventListener('resize', this.resize, false); + }, + methods: { + dragMove(e) { + if (!this.dragging) return; + + let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left; + const spaceLeft = 20; + const { clientWidth } = this.$refs.swipeFrame; + if (leftValue <= 0) { + leftValue = 0; + } else if (leftValue > clientWidth - spaceLeft) { + leftValue = clientWidth - spaceLeft; + } + + this.swipeWrapWidth = this.swipeMaxWidth - leftValue; + this.swipeBarPos = leftValue; + }, + startDrag() { + this.dragging = true; + document.body.style.userSelect = 'none'; + document.body.addEventListener('mousemove', this.dragMove); + }, + stopDrag() { + this.dragging = false; + document.body.style.userSelect = ''; + document.body.removeEventListener('mousemove', this.dragMove); + }, + prepareSwipe() { + if (this.swipeOldImgInfo && this.swipeNewImgInfo) { + // Add 2 for border width + this.swipeMaxWidth = + Math.max(this.swipeOldImgInfo.renderedWidth, this.swipeNewImgInfo.renderedWidth) + 2; + this.swipeWrapWidth = this.swipeMaxWidth; + this.swipeMaxHeight = + Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2; + + document.body.addEventListener('mouseup', this.stopDrag); + } + }, + swipeNewImgLoaded(imgInfo) { + this.swipeNewImgInfo = imgInfo; + this.prepareSwipe(); + }, + swipeOldImgLoaded(imgInfo) { + this.swipeOldImgInfo = imgInfo; + this.prepareSwipe(); + }, + resize: _.throttle(function throttledResize() { + this.swipeBarPos = 0; + }, 400), + }, +}; +</script> + +<template> + <div class="swipe view"> + <div + class="swipe-frame" + ref="swipeFrame" + :style="{ + 'width': swipeMaxPixelWidth, + 'height': swipeMaxPixelHeight, + }"> + <div class="frame deleted"> + <image-viewer + key="swipeOldImg" + ref="swipeOldImg" + :render-info="false" + :path="oldPath" + :project-path="projectPath" + @imgLoaded="swipeOldImgLoaded" + /> + </div> + <div + class="swipe-wrap" + ref="swipeWrap" + :style="{ + 'width': swipeWrapPixelWidth, + 'height': swipeMaxPixelHeight, + }"> + <div class="frame added"> + <image-viewer + key="swipeNewImg" + :render-info="false" + :path="newPath" + :project-path="projectPath" + @imgLoaded="swipeNewImgLoaded" + /> + </div> + </div> + <span + class="swipe-bar" + ref="swipeBar" + @mousedown="startDrag" + @mouseup="stopDrag" + :style="{ 'left': swipeBarPixelPos }"> + <span class="top-handle"></span> + <span class="bottom-handle"></span> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue new file mode 100644 index 00000000000..9c19266ecdf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue @@ -0,0 +1,41 @@ +<script> +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> + +<template> + <div class="two-up view row"> + <div class="col-sm-6 frame deleted"> + <image-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + <div class="col-sm-6 frame added"> + <image-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue new file mode 100644 index 00000000000..43b28f96a06 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue @@ -0,0 +1,109 @@ +<script> +import ImageViewer from '../../content_viewer/viewers/image_viewer.vue'; +import TwoUpViewer from './image_diff/two_up_viewer.vue'; +import SwipeViewer from './image_diff/swipe_viewer.vue'; +import OnionSkinViewer from './image_diff/onion_skin_viewer.vue'; +import { diffModes, imageViewMode } from '../constants'; + +export default { + components: { + ImageViewer, + TwoUpViewer, + SwipeViewer, + OnionSkinViewer, + }, + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + mode: imageViewMode.twoup, + }; + }, + methods: { + changeMode(newMode) { + this.mode = newMode; + }, + }, + diffModes, + imageViewMode, +}; +</script> + +<template> + <div class="diff-file-container"> + <div + class="diff-viewer" + v-if="diffMode === $options.diffModes.replaced"> + <div class="image js-replaced-image"> + <two-up-viewer + v-if="mode === $options.imageViewMode.twoup" + v-bind="$props"/> + <swipe-viewer + v-else-if="mode === $options.imageViewMode.swipe" + v-bind="$props"/> + <onion-skin-viewer + v-else-if="mode === $options.imageViewMode.onion" + v-bind="$props"/> + </div> + <div class="view-modes"> + <ul class="view-modes-menu"> + <li + :class="{ + active: mode === $options.imageViewMode.twoup + }" + @click="changeMode($options.imageViewMode.twoup)"> + {{ s__('ImageDiffViewer|2-up') }} + </li> + <li + :class="{ + active: mode === $options.imageViewMode.swipe + }" + @click="changeMode($options.imageViewMode.swipe)"> + {{ s__('ImageDiffViewer|Swipe') }} + </li> + <li + :class="{ + active: mode === $options.imageViewMode.onion + }" + @click="changeMode($options.imageViewMode.onion)"> + {{ s__('ImageDiffViewer|Onion skin') }} + </li> + </ul> + </div> + <div class="note-container"></div> + </div> + <div + v-else-if="diffMode === $options.diffModes.new" + class="diff-viewer added"> + <image-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + <div + v-else + class="diff-viewer deleted"> + <image-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js new file mode 100644 index 00000000000..02f28da8bb0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js @@ -0,0 +1,5 @@ +export function pixeliseValue(val) { + return val ? `${val}px` : ''; +} + +export default {}; |