diff --git a/DELETE-THIS-DEFORE-MERGE-snap-mirror-line-test.html b/DELETE-THIS-DEFORE-MERGE-snap-mirror-line-test.html
new file mode 100644
index 00000000..a6df07a6
--- /dev/null
+++ b/DELETE-THIS-DEFORE-MERGE-snap-mirror-line-test.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+
+
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 00000000..8a302d66
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,6 @@
+- [ ] Fix bugs on mobile browsers.
+ - [ ] Issue: TouchSensor: Why set pageX/Y to clientX/Y
+ - [x] Add pageX/Y to SensorEvent
+- [ ] event offset: Which coordiante given in API? Which coordiante use in code? \
+ coordiantes: page, client, container, mirror
+- [ ] `SnapMirror.line` edge case
diff --git a/examples/.prettierrc b/examples/.prettierrc
index 3f584f60..87e22d80 100644
--- a/examples/.prettierrc
+++ b/examples/.prettierrc
@@ -1,4 +1,7 @@
{
+ "trailingComma": "all",
"printWidth": 120,
- "singleQuote": true
+ "singleQuote": true,
+ "bracketSpacing": false,
+ "arrowParens": "always"
}
diff --git a/examples/src/content/Plugins/SnapMirror/SnapMirror.html b/examples/src/content/Plugins/SnapMirror/SnapMirror.html
new file mode 100644
index 00000000..5ba93aa4
--- /dev/null
+++ b/examples/src/content/Plugins/SnapMirror/SnapMirror.html
@@ -0,0 +1,19 @@
+{% import 'components/Block/Block.html' as Block %}
+
+{% macro render(id) %}
+
+
+
+ {{ Block.render('drag', {index: 1, draggable: true}) }}
+
+
+
+
+
+
+ {{ Block.render('drag', {index: 1, draggable: true}) }}
+ {{ Block.render('drop', {type: 'Hollow'}) }}
+
+
+
+{% endmacro %}
diff --git a/examples/src/content/Plugins/SnapMirror/SnapMirror.scss b/examples/src/content/Plugins/SnapMirror/SnapMirror.scss
new file mode 100644
index 00000000..f5698252
--- /dev/null
+++ b/examples/src/content/Plugins/SnapMirror/SnapMirror.scss
@@ -0,0 +1,76 @@
+////
+/// Content
+/// SnapMirror
+////
+
+@import 'utils/shared/functions';
+@import 'utils/shared/layout';
+
+.SnapMirror {
+ .Workspace {
+ $size: 50px;
+ $border-width: 0.3rem;
+ &.BlockLayout--typePositioned {
+ height: 32rem;
+ border: 0.6rem solid #212529;
+ overflow: auto;
+
+ .Block {
+ width: calc(#{3 * $size} + #{$border-width});
+ height: calc(#{3 * $size} + #{$border-width});
+ position: absolute;
+ }
+
+ &.draggable-container--over {
+ .Workspace__grid {
+ background: linear-gradient(180deg, #00f 0, #00f $border-width, transparent 0, transparent $size) 0px 0px /
+ 100% $size repeat-y,
+ linear-gradient(90deg, #00f 0, #00f $border-width, transparent 0, transparent $size) 0px 0px / #{$size} 100%
+ repeat-x;
+ }
+ }
+ }
+
+ &__grid {
+ width: 1500px;
+ height: 1000px;
+ background: linear-gradient(180deg, #000 0, #000 $border-width, transparent 0, transparent $size) 0px 0px / 100%
+ $size repeat-y,
+ linear-gradient(90deg, #000 0, #000 $border-width, transparent 0, transparent $size) 0px 0px / #{$size} 100% repeat-x;
+ }
+ }
+
+ .CircleRange {
+ &.BlockLayout--typePositioned {
+ height: 64rem;
+ border: 0.6rem solid #212529;
+ overflow: auto;
+
+ .Block--isDraggable {
+ position: absolute;
+ width: 20rem;
+ height: 20rem;
+ z-index: 2;
+ }
+
+ .Block--typeHollow {
+ position: absolute;
+ top: calc(50% - 10rem);
+ left: calc(50% - 10rem);
+ width: 20rem;
+ height: 20rem;
+ }
+
+ .circle {
+ position: absolute;
+ top: calc(50% - 25rem);
+ left: calc(50% - 25rem);
+ width: 50rem;
+ height: 50rem;
+ border: 3px solid #000;
+ border-radius: 50%;
+ content: '';
+ }
+ }
+ }
+}
diff --git a/examples/src/content/Plugins/SnapMirror/index.js b/examples/src/content/Plugins/SnapMirror/index.js
new file mode 100644
index 00000000..5707c70a
--- /dev/null
+++ b/examples/src/content/Plugins/SnapMirror/index.js
@@ -0,0 +1,95 @@
+// eslint-disable-next-line import/no-unresolved
+import {Draggable, Plugins} from '@shopify/draggable';
+
+function initCircle() {
+ const container = document.querySelector('#SnapMirror .BlockLayout.CircleRange');
+ const containerRect = container.getBoundingClientRect();
+ const circleRect = document.querySelector('.circle').getBoundingClientRect();
+
+ const targets = [];
+ [...document.querySelectorAll('.Block--typeHollow')].forEach((star) => {
+ const rect = star.getBoundingClientRect();
+ const range = circleRect.width / 2;
+ targets.push({
+ x: rect.x - containerRect.x + rect.width / 2,
+ y: rect.y - containerRect.y + rect.width / 2,
+ range(coord, target, relativePoint, {pointInMirrorCoordinate}) {
+ return (
+ (coord.x + pointInMirrorCoordinate.x - target.x) ** 2 +
+ (coord.y + pointInMirrorCoordinate.y - target.y) ** 2 <
+ range ** 2
+ );
+ },
+ });
+ });
+
+ console.log(targets);
+ const draggable = new Draggable([container], {
+ draggable: '.Block--isDraggable',
+ mirror: {
+ constrainDimensions: true,
+ },
+ plugins: [Plugins.SnapMirror],
+ SnapMirror: {
+ targets,
+ relativePoints: [{x: 0.5, y: 0.5}],
+ },
+ });
+
+ let originalSource;
+
+ draggable.on('mirror:create', (evt) => {
+ originalSource = evt.originalSource;
+ });
+
+ draggable.on('mirror:destroy', (evt) => {
+ if (evt.mirror.style.position !== 'absolute') {
+ return;
+ }
+ originalSource.style.transform = evt.mirror.style.transform;
+ });
+
+ return draggable;
+}
+
+function initWorkspace() {
+ const container = document.querySelector('#SnapMirror .BlockLayout.Workspace');
+
+ const draggable = new Draggable([container], {
+ draggable: '.Block--isDraggable',
+ mirror: {
+ constrainDimensions: true,
+ },
+ plugins: [Plugins.SnapMirror],
+ SnapMirror: {
+ targets: [
+ Plugins.SnapMirror.grid({
+ x: 50,
+ y: 50,
+ }),
+ ],
+ },
+ });
+
+ let originalSource;
+
+ draggable.on('mirror:create', (evt) => {
+ originalSource = evt.originalSource;
+ });
+
+ draggable.on('mirror:destroy', (evt) => {
+ if (evt.mirror.style.position !== 'absolute') {
+ return;
+ }
+ originalSource.style.transform = evt.mirror.style.transform;
+ });
+
+ return draggable;
+}
+
+export default function PluginsSnapMirror() {
+ const workspaceDraggable = initWorkspace();
+ const CircleDraggable = initCircle();
+
+ return [workspaceDraggable, CircleDraggable];
+}
diff --git a/examples/src/content/index.js b/examples/src/content/index.js
index e6a14bcf..8f9e363d 100644
--- a/examples/src/content/index.js
+++ b/examples/src/content/index.js
@@ -17,6 +17,7 @@ import PluginsCollidable from './Plugins/Collidable';
import PluginsSnappable from './Plugins/Snappable';
import PluginsSwapAnimation from './Plugins/SwapAnimation';
import PluginsSortAnimation from './Plugins/SortAnimation';
+import PluginsSnapMirror from './Plugins/SnapMirror';
const Content = {
Home,
@@ -32,6 +33,7 @@ const Content = {
PluginsSnappable,
PluginsSwapAnimation,
PluginsSortAnimation,
+ PluginsSnapMirror,
};
export default Content;
diff --git a/examples/src/styles/examples-app.scss b/examples/src/styles/examples-app.scss
index c61ec1fe..897c982f 100644
--- a/examples/src/styles/examples-app.scss
+++ b/examples/src/styles/examples-app.scss
@@ -59,3 +59,4 @@
@import 'content/Plugins/Snappable/Snappable';
@import 'content/Plugins/SwapAnimation/SwapAnimation';
@import 'content/Plugins/SortAnimation/SortAnimation';
+@import 'content/Plugins/SnapMirror/SnapMirror';
diff --git a/examples/src/views/data-pages.json b/examples/src/views/data-pages.json
index 0f07686f..004c79b1 100644
--- a/examples/src/views/data-pages.json
+++ b/examples/src/views/data-pages.json
@@ -32,7 +32,8 @@
"Collidable",
"Snappable",
"~Swap Animation",
- "Sort Animation"
+ "Sort Animation",
+ "Snap Mirror"
]
}
]
diff --git a/examples/src/views/snap-mirror.html b/examples/src/views/snap-mirror.html
new file mode 100644
index 00000000..0e0e2ed9
--- /dev/null
+++ b/examples/src/views/snap-mirror.html
@@ -0,0 +1,29 @@
+{% extends 'templates/document.html' %}
+
+{% import 'components/Document/Head.html' as Head %}
+{% import 'components/Sidebar/Sidebar.html' as Sidebar %}
+{% import 'components/PageHeader/PageHeader.html' as PageHeader %}
+
+{% import 'content/Plugins/SnapMirror/SnapMirror.html' as SnapMirror %}
+
+{% set ViewAttr = {
+ id: 'SnapMirror',
+ parent: 'Plugins',
+ child: 'Snap Mirror',
+ subheading: 'Enable snap mirror to target points by including the SnapMirror plugin. Drag an item into the range of a target point will snap to the point.'
+} %}
+
+{% block PageId %}{{ ViewAttr.id }}{% endblock %}
+
+{% block head %}
+{{ Head.render(ViewAttr) }}
+{% endblock %}
+
+{% block sidebar %}
+{{ Sidebar.render(ViewAttr, DataPages) }}
+{% endblock %}
+
+{% block main %}
+{{ PageHeader.render(ViewAttr) }}
+{{ SnapMirror.render(ViewAttr.id) }}
+{% endblock %}
diff --git a/scripts/build/bundles.js b/scripts/build/bundles.js
index 0c9838aa..b9fce27d 100644
--- a/scripts/build/bundles.js
+++ b/scripts/build/bundles.js
@@ -75,6 +75,13 @@ const bundles = [
source: 'Plugins/SortAnimation/index',
path: 'plugins/',
},
+
+ {
+ name: 'SnapMirror',
+ filename: 'snap-mirror',
+ source: 'Plugins/SnapMirror/index',
+ path: 'plugins/',
+ },
];
module.exports = {bundles};
diff --git a/scripts/test/helpers/constants.js b/scripts/test/helpers/constants.js
index dd966433..0ead6a86 100644
--- a/scripts/test/helpers/constants.js
+++ b/scripts/test/helpers/constants.js
@@ -1,6 +1,8 @@
export const defaultTouchEventOptions = {
touches: [
{
+ clientX: 0,
+ clientY: 0,
pageX: 0,
pageY: 0,
},
diff --git a/src/Draggable/Plugins/Mirror/Mirror.js b/src/Draggable/Plugins/Mirror/Mirror.js
index 96d461ec..6e18472f 100644
--- a/src/Draggable/Plugins/Mirror/Mirror.js
+++ b/src/Draggable/Plugins/Mirror/Mirror.js
@@ -13,7 +13,6 @@ export const onDragMove = Symbol('onDragMove');
export const onDragStop = Symbol('onDragStop');
export const onMirrorCreated = Symbol('onMirrorCreated');
export const onMirrorMove = Symbol('onMirrorMove');
-export const onScroll = Symbol('onScroll');
export const getAppendableContainer = Symbol('getAppendableContainer');
/**
@@ -67,31 +66,11 @@ export default class Mirror extends AbstractPlugin {
...this.getOptions(),
};
- /**
- * Scroll offset for touch devices because the mirror is positioned fixed
- * @property {Object} scrollOffset
- * @property {Number} scrollOffset.x
- * @property {Number} scrollOffset.y
- */
- this.scrollOffset = {x: 0, y: 0};
-
- /**
- * Initial scroll offset for touch devices because the mirror is positioned fixed
- * @property {Object} scrollOffset
- * @property {Number} scrollOffset.x
- * @property {Number} scrollOffset.y
- */
- this.initialScrollOffset = {
- x: window.scrollX,
- y: window.scrollY,
- };
-
this[onDragStart] = this[onDragStart].bind(this);
this[onDragMove] = this[onDragMove].bind(this);
this[onDragStop] = this[onDragStop].bind(this);
this[onMirrorCreated] = this[onMirrorCreated].bind(this);
this[onMirrorMove] = this[onMirrorMove].bind(this);
- this[onScroll] = this[onScroll].bind(this);
}
/**
@@ -131,15 +110,6 @@ export default class Mirror extends AbstractPlugin {
return;
}
- if ('ontouchstart' in window) {
- document.addEventListener('scroll', this[onScroll], true);
- }
-
- this.initialScrollOffset = {
- x: window.scrollX,
- y: window.scrollY,
- };
-
const {source, originalSource, sourceContainer, sensorEvent} = dragEvent;
// Last sensor position of mirror move
@@ -233,13 +203,6 @@ export default class Mirror extends AbstractPlugin {
}
[onDragStop](dragEvent) {
- if ('ontouchstart' in window) {
- document.removeEventListener('scroll', this[onScroll], true);
- }
-
- this.initialScrollOffset = {x: 0, y: 0};
- this.scrollOffset = {x: 0, y: 0};
-
if (!this.mirror) {
return;
}
@@ -261,13 +224,6 @@ export default class Mirror extends AbstractPlugin {
}
}
- [onScroll]() {
- this.scrollOffset = {
- x: window.scrollX - this.initialScrollOffset.x,
- y: window.scrollY - this.initialScrollOffset.y,
- };
- }
-
/**
* Mirror created handler
* @param {MirrorCreatedEvent} mirrorEvent
@@ -293,7 +249,6 @@ export default class Mirror extends AbstractPlugin {
source,
sensorEvent,
mirrorClass,
- scrollOffset: this.scrollOffset,
options: this.options,
passedThreshX: true,
passedThreshY: true,
@@ -337,7 +292,6 @@ export default class Mirror extends AbstractPlugin {
options: this.options,
initialX: this.initialX,
initialY: this.initialY,
- scrollOffset: this.scrollOffset,
passedThreshX: mirrorEvent.passedThreshX,
passedThreshY: mirrorEvent.passedThreshY,
lastMovedX: this.lastMovedX,
@@ -491,7 +445,6 @@ function positionMirror({withFrame = false, initial = false} = {}) {
mirrorOffset,
initialY,
initialX,
- scrollOffset,
options,
passedThreshX,
passedThreshY,
@@ -511,11 +464,11 @@ function positionMirror({withFrame = false, initial = false} = {}) {
if (mirrorOffset) {
const x = passedThreshX
- ? Math.round((sensorEvent.clientX - mirrorOffset.left - scrollOffset.x) / (options.thresholdX || 1)) *
+ ? Math.round((sensorEvent.clientX - mirrorOffset.left) / (options.thresholdX || 1)) *
(options.thresholdX || 1)
: Math.round(lastMovedX);
const y = passedThreshY
- ? Math.round((sensorEvent.clientY - mirrorOffset.top - scrollOffset.y) / (options.thresholdY || 1)) *
+ ? Math.round((sensorEvent.clientY - mirrorOffset.top) / (options.thresholdY || 1)) *
(options.thresholdY || 1)
: Math.round(lastMovedY);
diff --git a/src/Draggable/Plugins/Scrollable/Scrollable.js b/src/Draggable/Plugins/Scrollable/Scrollable.js
index 8af279d7..dc974a98 100644
--- a/src/Draggable/Plugins/Scrollable/Scrollable.js
+++ b/src/Draggable/Plugins/Scrollable/Scrollable.js
@@ -49,13 +49,11 @@ export default class Scrollable extends AbstractPlugin {
};
/**
- * Keeps current mouse position
- * @property {Object} currentMousePosition
- * @property {Number} currentMousePosition.clientX
- * @property {Number} currentMousePosition.clientY
+ * Keeps current sensor event
+ * @property {SensorEvent} currentSensorEvent
* @type {Object|null}
*/
- this.currentMousePosition = null;
+ this.currentSensorEvent = null;
/**
* Scroll animation frame
@@ -159,18 +157,7 @@ export default class Scrollable extends AbstractPlugin {
return;
}
- const sensorEvent = dragEvent.sensorEvent;
- const scrollOffset = {x: 0, y: 0};
-
- if ('ontouchstart' in window) {
- scrollOffset.y = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
- scrollOffset.x = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0;
- }
-
- this.currentMousePosition = {
- clientX: sensorEvent.clientX - scrollOffset.x,
- clientY: sensorEvent.clientY - scrollOffset.y,
- };
+ this.currentSensorEvent = dragEvent.sensorEvent;
this.scrollAnimationFrame = requestAnimationFrame(this[scroll]);
}
@@ -186,7 +173,7 @@ export default class Scrollable extends AbstractPlugin {
this.scrollableElement = null;
this.scrollAnimationFrame = null;
this.findScrollableElementFrame = null;
- this.currentMousePosition = null;
+ this.currentSensorEvent = null;
}
/**
@@ -194,7 +181,7 @@ export default class Scrollable extends AbstractPlugin {
* @private
*/
[scroll]() {
- if (!this.scrollableElement || !this.currentMousePosition) {
+ if (!this.scrollableElement || !this.currentSensorEvent) {
return;
}
@@ -209,8 +196,7 @@ export default class Scrollable extends AbstractPlugin {
const documentScrollingElement = getDocumentScrollingElement();
const scrollableElement = this.scrollableElement;
- const clientX = this.currentMousePosition.clientX;
- const clientY = this.currentMousePosition.clientY;
+ const {clientX, clientY} = this.currentSensorEvent;
if (scrollableElement !== document.body && scrollableElement !== document.documentElement && !cutOff) {
const {offsetHeight, offsetWidth} = scrollableElement;
diff --git a/src/Draggable/Sensors/MouseSensor/MouseSensor.js b/src/Draggable/Sensors/MouseSensor/MouseSensor.js
index 2c28225c..9a737abe 100644
--- a/src/Draggable/Sensors/MouseSensor/MouseSensor.js
+++ b/src/Draggable/Sensors/MouseSensor/MouseSensor.js
@@ -109,6 +109,8 @@ export default class MouseSensor extends Sensor {
const container = this.currentContainer;
const dragStartEvent = new DragStartSensorEvent({
+ pageX: startEvent.pageX,
+ pageY: startEvent.pageY,
clientX: startEvent.clientX,
clientY: startEvent.clientY,
target: startEvent.target,
@@ -166,6 +168,8 @@ export default class MouseSensor extends Sensor {
const target = document.elementFromPoint(event.clientX, event.clientY);
const dragMoveEvent = new DragMoveSensorEvent({
+ pageX: event.pageX,
+ pageY: event.pageY,
clientX: event.clientX,
clientY: event.clientY,
target,
@@ -199,6 +203,8 @@ export default class MouseSensor extends Sensor {
const target = document.elementFromPoint(event.clientX, event.clientY);
const dragStopEvent = new DragStopSensorEvent({
+ pageX: event.pageX,
+ pageY: event.pageY,
clientX: event.clientX,
clientY: event.clientY,
target,
diff --git a/src/Draggable/Sensors/SensorEvent/SensorEvent.js b/src/Draggable/Sensors/SensorEvent/SensorEvent.js
index d5aa4559..0c0fa8e7 100644
--- a/src/Draggable/Sensors/SensorEvent/SensorEvent.js
+++ b/src/Draggable/Sensors/SensorEvent/SensorEvent.js
@@ -37,6 +37,26 @@ export class SensorEvent extends AbstractEvent {
return this.data.clientY;
}
+ /**
+ * Normalized pageX for both touch and mouse events
+ * @property pageX
+ * @type {Number}
+ * @readonly
+ */
+ get pageX() {
+ return this.data.pageX;
+ }
+
+ /**
+ * Normalized pageY for both touch and mouse events
+ * @property pageY
+ * @type {Number}
+ * @readonly
+ */
+ get pageY() {
+ return this.data.pageY;
+ }
+
/**
* Normalized target for both touch and mouse events
* Returns the element that is behind cursor or touch pointer
diff --git a/src/Draggable/Sensors/TouchSensor/TouchSensor.js b/src/Draggable/Sensors/TouchSensor/TouchSensor.js
index 6e4bc31d..1116c8f9 100644
--- a/src/Draggable/Sensors/TouchSensor/TouchSensor.js
+++ b/src/Draggable/Sensors/TouchSensor/TouchSensor.js
@@ -140,11 +140,13 @@ export default class TouchSensor extends Sensor {
[startDrag]() {
const startEvent = this.startEvent;
const container = this.currentContainer;
- const touch = touchCoords(startEvent);
+ const {clientX, clientY, pageX, pageY} = touchCoords(startEvent);
const dragStartEvent = new DragStartSensorEvent({
- clientX: touch.pageX,
- clientY: touch.pageY,
+ clientX,
+ clientY,
+ pageX,
+ pageY,
target: startEvent.target,
container,
originalEvent: startEvent,
@@ -190,12 +192,14 @@ export default class TouchSensor extends Sensor {
if (!this.dragging) {
return;
}
- const {pageX, pageY} = touchCoords(event);
+ const {clientX, clientY, pageX, pageY} = touchCoords(event);
const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY);
const dragMoveEvent = new DragMoveSensorEvent({
- clientX: pageX,
- clientY: pageY,
+ clientX,
+ clientY,
+ pageX,
+ pageY,
target,
container: this.currentContainer,
originalEvent: event,
@@ -227,14 +231,16 @@ export default class TouchSensor extends Sensor {
document.removeEventListener('touchmove', this[onTouchMove]);
- const {pageX, pageY} = touchCoords(event);
+ const {clientX, clientY, pageX, pageY} = touchCoords(event);
const target = document.elementFromPoint(pageX - window.scrollX, pageY - window.scrollY);
event.preventDefault();
const dragStopEvent = new DragStopSensorEvent({
- clientX: pageX,
- clientY: pageY,
+ clientX,
+ clientY,
+ pageX,
+ pageY,
target,
container: this.currentContainer,
originalEvent: event,
diff --git a/src/Draggable/Sensors/TouchSensor/tests/TouchSensor.test.js b/src/Draggable/Sensors/TouchSensor/tests/TouchSensor.test.js
index a094d05f..fc12e3a1 100644
--- a/src/Draggable/Sensors/TouchSensor/tests/TouchSensor.test.js
+++ b/src/Draggable/Sensors/TouchSensor/tests/TouchSensor.test.js
@@ -97,6 +97,31 @@ describe('TouchSensor', () => {
expect(touchEndEvent.defaultPrevented).toBe(true);
});
+
+ it('event attributes should be set correctly', () => {
+ const touchEvent = {
+ pageX: 21,
+ pageY: 22,
+ clientX: 11,
+ clientY: 12,
+ };
+
+ function testAttributes(event) {
+ expect(event.detail.clientX).toBe(touchEvent.clientX);
+ expect(event.detail.clientY).toBe(touchEvent.clientY);
+ }
+
+ sandbox.addEventListener('drag:start', testAttributes);
+
+ sandbox.addEventListener('drag:move', testAttributes);
+
+ sandbox.addEventListener('drag:stop', testAttributes);
+
+ touchStart(draggableElement, {touches: [touchEvent]});
+ waitForDragDelay();
+ touchMove(draggableElement, {touches: [touchEvent]});
+ touchRelease(draggableElement, {touches: [touchEvent]});
+ });
});
describe('using distance', () => {
diff --git a/src/Plugins/SnapMirror/README.md b/src/Plugins/SnapMirror/README.md
new file mode 100644
index 00000000..57ef1c3d
--- /dev/null
+++ b/src/Plugins/SnapMirror/README.md
@@ -0,0 +1,89 @@
+## SnapMirror
+
+The SnapMirror plugin snap the mirror to the target points.
+
+This plugin is not included in the default Draggable bundle, so you'll need to import it separately.
+
+
+
+### Import
+
+```js
+import {Plugins} from '@shopify/draggable';
+```
+
+```js
+import SnapMirror from '@shopify/draggable/lib/plugins/snap-mirror';
+```
+
+```html
+
+```
+
+```html
+
+```
+
+The over container should set relative/absolute/fixed position, bacause while over a container mirror using absolute position based on the container.
+
+```css
+.draggable-container--over {
+ position: relative;
+}
+```
+
+### Options
+
+**`targets {Array