summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue12
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue20
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue13
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue31
-rw-r--r--app/assets/javascripts/ide/constants.js12
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js14
-rw-r--r--app/assets/javascripts/ide/stores/utils.js2
-rw-r--r--app/assets/javascripts/milestone_select.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue70
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue69
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue160
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue158
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue41
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js5
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 {};