summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Zallmann <tzallmann@gitlab.com>2018-06-13 09:35:52 +0000
committerPhil Hughes <me@iamphill.com>2018-06-13 09:35:52 +0000
commit8a23bcc9bc0f7ab453ee09d41a9407d40d57ba4c (patch)
tree53d6ddeaa1202ff3f02a71c95601606f4da62dea
parent929f12b61a6dbda7cd417ac2dd90cf9d3c94ad1c (diff)
downloadgitlab-ce-8a23bcc9bc0f7ab453ee09d41a9407d40d57ba4c.tar.gz
Image Diff Viewing + Download Diff Viewing
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue31
-rw-r--r--app/assets/javascripts/ide/constants.js7
-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/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
-rw-r--r--app/assets/stylesheets/framework/files.scss48
-rw-r--r--app/assets/stylesheets/pages/diff.scss132
-rw-r--r--app/assets/stylesheets/pages/repo.scss1
-rw-r--r--changelogs/unreleased/tz-diff-blob-image-viewer.yml5
-rw-r--r--spec/javascripts/fixtures/images/green_box.pngbin0 -> 1306 bytes
-rw-r--r--spec/javascripts/fixtures/images/red_box.pngbin0 -> 1305 bytes
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js47
-rw-r--r--spec/javascripts/test_constants.js3
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js10
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js70
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js185
-rw-r--r--spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js13
26 files changed, 1204 insertions, 36 deletions
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index d365745d78b..96f181ff79d 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="multi-file-editor-holder"
:class="{
@@ -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..64271ca56d1 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -21,6 +21,13 @@ 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',
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/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index 7b5367ac19b..c274d3ab590 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 {};
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index f77ec4b6a2c..f060254777c 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -400,3 +400,51 @@ span.idiff {
color: $common-gray-light;
border: 1px solid $common-gray-light;
}
+
+.preview-container {
+ height: 100%;
+ overflow: auto;
+
+ .file-container {
+ background-color: $gray-darker;
+ display: flex;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+
+ text-align: center;
+
+ .file-content {
+ padding: $gl-padding;
+ max-width: 100%;
+ max-height: 100%;
+
+ img {
+ max-width: 90%;
+ max-height: 70vh;
+ }
+
+ .is-zoomable {
+ cursor: pointer;
+ cursor: zoom-in;
+
+ &.is-zoomed {
+ cursor: pointer;
+ cursor: zoom-out;
+ max-width: none;
+ max-height: none;
+ margin-right: $gl-padding;
+ }
+ }
+ }
+
+ .file-info {
+ font-size: $label-font-size;
+ color: $diff-image-info-color;
+ }
+ }
+
+ .md-previewer {
+ padding: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index f06c9dcdf8c..fbc97ec0c95 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -189,8 +189,22 @@
img {
border: 1px solid $white-light;
- background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%),
- linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%);
+ background-image: linear-gradient(
+ 45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%
+ ),
+ linear-gradient(
+ 45deg,
+ $border-color 25%,
+ transparent 25%,
+ transparent 75%,
+ $border-color 75%,
+ $border-color 100%
+ );
background-size: 10px 10px;
background-position: 0 0, 5px 5px;
max-width: 100%;
@@ -395,6 +409,69 @@
.line_content {
white-space: pre-wrap;
}
+
+ .diff-file-container {
+ .frame.deleted {
+ border: 0;
+ background-color: inherit;
+
+ .image_file img {
+ border: 1px solid $deleted;
+ }
+ }
+
+ .frame.added {
+ border: 0;
+ background-color: inherit;
+
+ .image_file img {
+ border: 1px solid $added;
+ }
+ }
+
+ .swipe.view,
+ .onion-skin.view {
+ .swipe-wrap {
+ top: 0;
+ right: 0;
+ }
+
+ .frame.deleted {
+ top: 0;
+ right: 0;
+ }
+
+ .swipe-bar {
+ top: 0;
+
+ .top-handle {
+ top: -14px;
+ left: -7px;
+ }
+
+ .bottom-handle {
+ bottom: -14px;
+ left: -7px;
+ }
+ }
+
+ .file-container {
+ display: inline-block;
+
+ .file-content {
+ padding: 0;
+
+ img {
+ max-width: none;
+ }
+ }
+ }
+ }
+
+ .onion-skin.view .controls {
+ bottom: -25px;
+ }
+ }
}
.file-content .diff-file {
@@ -536,7 +613,7 @@
margin-right: 0;
border-color: $white-light;
cursor: pointer;
- transition: all .1s ease-out;
+ transition: all 0.1s ease-out;
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
@@ -563,7 +640,7 @@
height: 24px;
border-radius: 50%;
padding: 0;
- transition: transform .1s ease-out;
+ transition: transform 0.1s ease-out;
z-index: 100;
.collapse-icon {
@@ -708,11 +785,35 @@
width: 100%;
height: 10px;
background-color: $white-light;
- background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%),
- linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%);
- background-position: 5px 5px,0 5px,0 5px,5px 5px;
+ background-image: linear-gradient(
+ 45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ 225deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ 135deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ ),
+ linear-gradient(
+ -45deg,
+ transparent,
+ transparent 73%,
+ $diff-jagged-border-gradient-color 75%,
+ $white-light 80%
+ );
+ background-position: 5px 5px, 0 5px, 0 5px, 5px 5px;
background-size: 10px 10px;
background-repeat: repeat;
}
@@ -750,11 +851,16 @@
.frame.click-to-comment {
position: relative;
cursor: image-url('illustrations/image_comment_light_cursor.svg')
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ auto;
// Retina cursor
- cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x)
- $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
+ cursor: -webkit-image-set(
+ image-url('illustrations/image_comment_light_cursor.svg') 1x,
+ image-url('illustrations/image_comment_light_cursor@2x.svg') 2x
+ )
+ $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ auto;
.comment-indicator {
position: absolute;
@@ -840,7 +946,7 @@
.diff-notes-collapse,
.note,
- .discussion-reply-holder, {
+ .discussion-reply-holder {
display: none;
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 6e7fc50c63d..0fb95572cfd 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -335,7 +335,6 @@
img {
max-width: 90%;
- max-height: 90%;
}
.isZoomable {
diff --git a/changelogs/unreleased/tz-diff-blob-image-viewer.yml b/changelogs/unreleased/tz-diff-blob-image-viewer.yml
new file mode 100644
index 00000000000..81d87bc71f5
--- /dev/null
+++ b/changelogs/unreleased/tz-diff-blob-image-viewer.yml
@@ -0,0 +1,5 @@
+---
+title: Web IDE supports now Image + Download Diff Viewing
+merge_request: 18768
+author:
+type: added
diff --git a/spec/javascripts/fixtures/images/green_box.png b/spec/javascripts/fixtures/images/green_box.png
new file mode 100644
index 00000000000..cd1ff9f9ade
--- /dev/null
+++ b/spec/javascripts/fixtures/images/green_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/images/red_box.png b/spec/javascripts/fixtures/images/red_box.png
new file mode 100644
index 00000000000..73b2927da0f
--- /dev/null
+++ b/spec/javascripts/fixtures/images/red_box.png
Binary files differ
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index e83961fcedc..52f83be8e8c 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -152,6 +152,53 @@ describe('IDE store file mutations', () => {
expect(localFile.mrChange.diff).toBe('ABC');
});
+
+ it('has diffMode replaced by default', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('replaced');
+ });
+
+ it('has diffMode new', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ new_file: true,
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('new');
+ });
+
+ it('has diffMode deleted', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ deleted_file: true,
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('deleted');
+ });
+
+ it('has diffMode renamed', () => {
+ mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+ file: localFile,
+ mrChange: {
+ diff: 'ABC',
+ renamed_file: true,
+ },
+ });
+
+ expect(localFile.mrChange.diffMode).toBe('renamed');
+ });
});
describe('DISCARD_FILE_CHANGES', () => {
diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js
index df59195e9f6..a820dd2d09c 100644
--- a/spec/javascripts/test_constants.js
+++ b/spec/javascripts/test_constants.js
@@ -2,3 +2,6 @@ export const FIXTURES_PATH = '/base/spec/javascripts/fixtures';
export const TEST_HOST = 'http://test.host';
export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`;
+
+export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/green_box.png`;
+export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/red_box.png`;
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
index 383f0cd29ea..e2c34508b0d 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
describe('ContentViewer', () => {
let vm;
@@ -41,12 +42,12 @@ describe('ContentViewer', () => {
it('renders image preview', done => {
createComponent({
- path: 'test.jpg',
+ path: GREEN_BOX_IMAGE_URL,
fileSize: 1024,
});
setTimeout(() => {
- expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe('test.jpg');
+ expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
done();
});
@@ -59,9 +60,8 @@ describe('ContentViewer', () => {
});
setTimeout(() => {
- expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain(
- 'test.abc (1.00 KiB)',
- );
+ expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('test.abc');
+ expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('(1.00 KiB)');
expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toContain('Download');
done();
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
new file mode 100644
index 00000000000..71d9145bf22
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+
+describe('DiffViewer', () => {
+ let vm;
+
+ function createComponent(props) {
+ const DiffViewer = Vue.extend(diffViewer);
+ vm = mountComponent(DiffViewer, props);
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders image diff', done => {
+ window.gon = {
+ relative_url_root: '',
+ };
+
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ newSha: 'ABC',
+ oldPath: RED_BOX_IMAGE_URL,
+ oldSha: 'DEF',
+ projectPath: '',
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ `//raw/DEF/${RED_BOX_IMAGE_URL}`,
+ );
+
+ expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ `//raw/ABC/${GREEN_BOX_IMAGE_URL}`,
+ );
+
+ done();
+ });
+ });
+
+ it('renders fallback download diff display', done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: 'test.abc',
+ newSha: 'ABC',
+ oldPath: 'testold.abc',
+ oldSha: 'DEF',
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
+ 'testold.abc',
+ );
+ expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
new file mode 100644
index 00000000000..b878286ae3f
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -0,0 +1,185 @@
+import Vue from 'vue';
+import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+
+describe('ImageDiffViewer', () => {
+ let vm;
+
+ function createComponent(props) {
+ const ImageDiffViewer = Vue.extend(imageDiffViewer);
+ vm = mountComponent(ImageDiffViewer, props);
+ }
+
+ const triggerEvent = (eventName, el = vm.$el, clientX = 0) => {
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(
+ eventName,
+ true,
+ true,
+ window,
+ 1,
+ clientX,
+ 0,
+ clientX,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null,
+ );
+
+ el.dispatchEvent(event);
+ };
+
+ const dragSlider = (sliderElement, dragPixel = 20) => {
+ triggerEvent('mousedown', sliderElement);
+ triggerEvent('mousemove', document.body, dragPixel);
+ triggerEvent('mouseup', document.body);
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders image diff for replaced', done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ GREEN_BOX_IMAGE_URL,
+ );
+ expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ RED_BOX_IMAGE_URL,
+ );
+
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up');
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe(
+ 'Swipe',
+ );
+ expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe(
+ 'Onion skin',
+ );
+
+ done();
+ });
+ });
+
+ it('renders image diff for new', done => {
+ createComponent({
+ diffMode: 'new',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: '',
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe(
+ GREEN_BOX_IMAGE_URL,
+ );
+
+ done();
+ });
+ });
+
+ it('renders image diff for deleted', done => {
+ createComponent({
+ diffMode: 'deleted',
+ newPath: '',
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe(
+ RED_BOX_IMAGE_URL,
+ );
+
+ done();
+ });
+ });
+
+ describe('swipeMode', () => {
+ beforeEach(done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('switches to Swipe Mode', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe');
+ done();
+ });
+ });
+
+ it('drag handler is working', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('1px');
+ expect(vm.$el.querySelector('.top-handle')).not.toBeNull();
+
+ dragSlider(vm.$el.querySelector('.swipe-bar'), 40);
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('-20px');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('onionSkin', () => {
+ beforeEach(done => {
+ createComponent({
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('switches to Onion Skin Mode', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe(
+ 'Onion skin',
+ );
+ done();
+ });
+ });
+
+ it('has working drag handler', done => {
+ vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dragger').style.left).toBe('100px');
+
+ dragSlider(vm.$el.querySelector('.dragger'));
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.dragger').style.left).toBe('20px');
+ expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js b/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js
new file mode 100644
index 00000000000..2388660b0c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js
@@ -0,0 +1,13 @@
+import * as domUtils from '~/vue_shared/components/lib/utils/dom_utils';
+
+describe('domUtils', () => {
+ describe('pixeliseValue', () => {
+ it('should add px to a given Number', () => {
+ expect(domUtils.pixeliseValue(12)).toEqual('12px');
+ });
+
+ it('should not add px to 0', () => {
+ expect(domUtils.pixeliseValue(0)).toEqual('');
+ });
+ });
+});