Commit 55dee38f authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'tq/click-drag-design-explore' into 'master'

Design view: Implement design exploration via click-and-drag

See merge request gitlab-org/gitlab!25405
parents c62bd48e b5cbec88
......@@ -126,6 +126,8 @@ to help summarize changes between versions.
Designs can be explored in greater detail by zooming in and out of the image.
Control the amount of zoom with the `+` and `-` buttons at the bottom of the image.
While zoomed, you can still [start new discussions](#starting-discussions-on-designs) on the image, and see any existing ones.
[Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197324) in GitLab 12.10, while zoomed in,
you can click-and-drag on the image to move around it.
![Design zooming](img/design_zooming_v12_7.png)
......
......@@ -25,6 +25,11 @@ export default {
required: false,
default: null,
},
disableCommenting: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -34,7 +39,10 @@ export default {
},
computed: {
overlayStyle() {
const cursor = this.disableCommenting ? 'unset' : undefined;
return {
cursor,
width: `${this.dimensions.width}px`,
height: `${this.dimensions.height}px`,
...this.position,
......@@ -207,6 +215,7 @@ export default {
@mouseleave="onNoteMouseup"
>
<button
v-show="!disableCommenting"
type="button"
class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
data-qa-selector="design_image_button"
......@@ -222,15 +231,15 @@ export default {
? getNotePositionStyle(movingNoteNewPosition)
: getNotePositionStyle(note.position)
"
@mousedown="onNoteMousedown($event, note)"
@mouseup="onNoteMouseup"
@mousedown.stop="onNoteMousedown($event, note)"
@mouseup.stop="onNoteMouseup"
/>
<design-note-pin
v-if="currentCommentForm"
:position="currentCommentPositionStyle"
:repositioning="isMovingCurrentComment"
@mousedown="onNoteMousedown"
@mouseup="onNoteMouseup"
@mousedown.stop="onNoteMousedown"
@mouseup.stop="onNoteMouseup"
/>
</div>
</template>
......@@ -46,6 +46,7 @@ export default {
height: 0,
},
initialLoad: true,
lastDragPosition: null,
};
},
computed: {
......@@ -55,6 +56,14 @@ export default {
currentCommentForm() {
return (this.isAnnotating && this.currentAnnotationPosition) || null;
},
presentationStyle() {
return {
cursor: this.isDraggingDesign ? 'grabbing' : undefined,
};
},
isDraggingDesign() {
return Boolean(this.lastDragPosition);
},
},
beforeDestroy() {
const { presentationViewport } = this.$refs;
......@@ -206,12 +215,60 @@ export default {
const position = this.getAnnotationPositon(coordinates);
this.$emit('moveNote', { noteId, discussionId, position });
},
onPresentationMousedown({ clientX, clientY }) {
if (!this.isDesignOverflowing()) return;
this.lastDragPosition = {
x: clientX,
y: clientY,
};
},
onPresentationMousemove({ clientX, clientY }) {
if (!this.lastDragPosition) return;
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
const { scrollLeft, scrollTop } = presentationViewport;
const deltaX = this.lastDragPosition.x - clientX;
const deltaY = this.lastDragPosition.y - clientY;
presentationViewport.scrollTo(scrollLeft + deltaX, scrollTop + deltaY);
this.lastDragPosition = {
x: clientX,
y: clientY,
};
},
onPresentationMouseup() {
this.lastDragPosition = null;
},
isDesignOverflowing() {
const { presentationContainer } = this.$refs;
if (!presentationContainer) return false;
return (
presentationContainer.scrollWidth > presentationContainer.offsetWidth ||
presentationContainer.scrollHeight > presentationContainer.offsetHeight
);
},
},
};
</script>
<template>
<div ref="presentationViewport" class="h-100 w-100 p-3 overflow-auto position-relative">
<div
ref="presentationViewport"
class="h-100 w-100 p-3 overflow-auto position-relative"
:style="presentationStyle"
@mousedown="onPresentationMousedown"
@mousemove="onPresentationMousemove"
@mouseup="onPresentationMouseup"
@mouseleave="onPresentationMouseup"
@touchstart="onPresentationMousedown"
@touchmove="onPresentationMousemove"
@touchend="onPresentationMouseup"
@touchcancel="onPresentationMouseup"
>
<div
ref="presentationContainer"
class="h-100 w-100 d-flex align-items-center position-relative"
......@@ -229,6 +286,7 @@ export default {
:position="overlayPosition"
:notes="discussionStartingNotes"
:current-comment-form="currentCommentForm"
:disable-commenting="isDraggingDesign"
@openCommentForm="openCommentForm"
@moveNote="moveNote"
/>
......
---
title: Add ability to explore zoomed in designs via click-and-drag
merge_request: 25405
author:
type: added
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design presentation component currentCommentForm currentCommentForm is equal to current annotation position when isAnnotating is true 1`] = `
exports[`Design management design presentation component currentCommentForm is equal to current annotation position when isAnnotating is true 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto position-relative"
>
......@@ -23,7 +23,7 @@ exports[`Design management design presentation component currentCommentForm curr
</div>
`;
exports[`Design management design presentation component currentCommentForm currentCommentForm is null when isAnnotating is false 1`] = `
exports[`Design management design presentation component currentCommentForm is null when isAnnotating is false 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto position-relative"
>
......@@ -45,7 +45,7 @@ exports[`Design management design presentation component currentCommentForm curr
</div>
`;
exports[`Design management design presentation component currentCommentForm currentCommentForm is null when isAnnotating is true but annotation position is falsey 1`] = `
exports[`Design management design presentation component currentCommentForm is null when isAnnotating is true but annotation position is falsey 1`] = `
<div
class="h-100 w-100 p-3 overflow-auto position-relative"
>
......
......@@ -31,6 +31,58 @@ describe('Design management design presentation component', () => {
wrapper.setData(data);
}
/**
* Spy on $refs and mock given values
* @param {Object} viewportDimensions {width, height}
* @param {Object} childDimensions {width, height}
* @param {Float} scrollTopPerc 0 < x < 1
* @param {Float} scrollLeftPerc 0 < x < 1
*/
function mockRefDimensions(
ref,
viewportDimensions,
childDimensions,
scrollTopPerc,
scrollLeftPerc,
) {
jest.spyOn(ref, 'scrollWidth', 'get').mockReturnValue(childDimensions.width);
jest.spyOn(ref, 'scrollHeight', 'get').mockReturnValue(childDimensions.height);
jest.spyOn(ref, 'offsetWidth', 'get').mockReturnValue(viewportDimensions.width);
jest.spyOn(ref, 'offsetHeight', 'get').mockReturnValue(viewportDimensions.height);
jest
.spyOn(ref, 'scrollLeft', 'get')
.mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc);
jest
.spyOn(ref, 'scrollTop', 'get')
.mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc);
}
function clickDragExplore(startCoords, endCoords, { useTouchEvents } = {}) {
const event = useTouchEvents
? {
mousedown: 'touchstart',
mousemove: 'touchmove',
mouseup: 'touchend',
}
: {
mousedown: 'mousedown',
mousemove: 'mousemove',
mouseup: 'mouseup',
};
wrapper.trigger(event.mousedown, {
clientX: startCoords.clientX,
clientY: startCoords.clientY,
});
return wrapper.vm.$nextTick().then(() => {
wrapper.trigger(event.mousemove, {
clientX: endCoords.clientX,
clientY: endCoords.clientY,
});
return wrapper.vm.$nextTick();
});
}
afterEach(() => {
wrapper.destroy();
});
......@@ -76,7 +128,7 @@ describe('Design management design presentation component', () => {
});
describe('currentCommentForm', () => {
it('currentCommentForm is null when isAnnotating is false', () => {
it('is null when isAnnotating is false', () => {
createComponent(
{
image: 'test.jpg',
......@@ -91,7 +143,7 @@ describe('Design management design presentation component', () => {
});
});
it('currentCommentForm is null when isAnnotating is true but annotation position is falsey', () => {
it('is null when isAnnotating is true but annotation position is falsey', () => {
createComponent(
{
image: 'test.jpg',
......@@ -107,7 +159,7 @@ describe('Design management design presentation component', () => {
});
});
it('currentCommentForm is equal to current annotation position when isAnnotating is true', () => {
it('is equal to current annotation position when isAnnotating is true', () => {
createComponent(
{
image: 'test.jpg',
......@@ -124,6 +176,7 @@ describe('Design management design presentation component', () => {
},
},
);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.currentCommentForm).toEqual({
x: 1,
......@@ -190,39 +243,6 @@ describe('Design management design presentation component', () => {
});
describe('getViewportCenter', () => {
/**
* Spy on $refs.presentationViewport with given values
* @param {Object} viewportDimensions {width, height}
* @param {Object} childDimensions {width, height}
* @param {Float} scrollTopPerc 0 < x < 1
* @param {Float} scrollLeftPerc 0 < x < 1
*/
const spyOnPresentationViewport = (
viewportDimensions,
childDimensions,
scrollTopPerc,
scrollLeftPerc,
) => {
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollWidth', 'get')
.mockReturnValue(childDimensions.width);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollHeight', 'get')
.mockReturnValue(childDimensions.height);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get')
.mockReturnValue(viewportDimensions.width);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get')
.mockReturnValue(viewportDimensions.height);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollLeft', 'get')
.mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc);
jest
.spyOn(wrapper.vm.$refs.presentationViewport, 'scrollTop', 'get')
.mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc);
};
beforeEach(() => {
createComponent(
{
......@@ -234,7 +254,13 @@ describe('Design management design presentation component', () => {
});
it('calculate center correctly with no scroll', () => {
spyOnPresentationViewport({ width: 10, height: 10 }, { width: 20, height: 20 }, 0, 0);
mockRefDimensions(
wrapper.vm.$refs.presentationViewport,
{ width: 10, height: 10 },
{ width: 20, height: 20 },
0,
0,
);
expect(wrapper.vm.getViewportCenter()).toEqual({
x: 5,
......@@ -243,7 +269,13 @@ describe('Design management design presentation component', () => {
});
it('calculate center correctly with some scroll', () => {
spyOnPresentationViewport({ width: 10, height: 10 }, { width: 20, height: 20 }, 0.5, 0.5);
mockRefDimensions(
wrapper.vm.$refs.presentationViewport,
{ width: 10, height: 10 },
{ width: 20, height: 20 },
0.5,
0.5,
);
expect(wrapper.vm.getViewportCenter()).toEqual({
x: 10,
......@@ -252,7 +284,13 @@ describe('Design management design presentation component', () => {
});
it('Returns default case if no overflow (scrollWidth==offsetWidth, etc.)', () => {
spyOnPresentationViewport({ width: 20, height: 20 }, { width: 20, height: 20 }, 0.5, 0.5);
mockRefDimensions(
wrapper.vm.$refs.presentationViewport,
{ width: 20, height: 20 },
{ width: 20, height: 20 },
0.5,
0.5,
);
expect(wrapper.vm.getViewportCenter()).toEqual({
x: 10,
......@@ -262,7 +300,7 @@ describe('Design management design presentation component', () => {
});
describe('scaleZoomFocalPoint', () => {
it('scaleZoomFocalPoint scales focal point correctly when zooming in', () => {
it('scales focal point correctly when zooming in', () => {
createComponent(
{
image: 'test.jpg',
......@@ -288,7 +326,7 @@ describe('Design management design presentation component', () => {
});
});
it('scaleZoomFocalPoint scales focal point correctly when zooming out', () => {
it('scales focal point correctly when zooming out', () => {
createComponent(
{
image: 'test.jpg',
......@@ -346,4 +384,77 @@ describe('Design management design presentation component', () => {
});
});
});
describe('onPresentationMousedown', () => {
it.each`
scenario | width | height
${'width overflows'} | ${101} | ${100}
${'height overflows'} | ${100} | ${101}
${'width and height overflows'} | ${200} | ${200}
`('sets lastDragPosition when design $scenario', ({ width, height }) => {
createComponent();
mockRefDimensions(
wrapper.vm.$refs.presentationContainer,
{ width: 100, height: 100 },
{ width, height },
);
const newLastDragPosition = { x: 2, y: 2 };
wrapper.vm.onPresentationMousedown({
clientX: newLastDragPosition.x,
clientY: newLastDragPosition.y,
});
expect(wrapper.vm.lastDragPosition).toStrictEqual(newLastDragPosition);
});
it('does not set lastDragPosition if design does not overflow', () => {
const lastDragPosition = { x: 1, y: 1 };
createComponent({}, { lastDragPosition });
mockRefDimensions(
wrapper.vm.$refs.presentationContainer,
{ width: 100, height: 100 },
{ width: 50, height: 50 },
);
wrapper.vm.onPresentationMousedown({ clientX: 2, clientY: 2 });
// check lastDragPosition is unchanged
expect(wrapper.vm.lastDragPosition).toStrictEqual(lastDragPosition);
});
});
describe('when clicking and dragging', () => {
it.each`
description | useTouchEvents
${'with touch events'} | ${true}
${'without touch events'} | ${false}
`('calls scrollTo with correct arguments $description', ({ useTouchEvents }) => {
createComponent(
{
image: 'test.jpg',
imageName: 'test',
},
mockOverlayData,
);
mockRefDimensions(
wrapper.vm.$refs.presentationContainer,
{ width: 10, height: 10 },
{ width: 20, height: 20 },
0,
0,
);
wrapper.element.scrollTo = jest.fn();
return clickDragExplore(
{ clientX: 0, clientY: 0 },
{ clientX: 10, clientY: 10 },
{ useTouchEvents },
).then(() => {
expect(wrapper.element.scrollTo).toHaveBeenCalledTimes(1);
expect(wrapper.element.scrollTo).toHaveBeenCalledWith(-10, -10);
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment