Browse Source

Merge branch 'drag-rules' into dev

pull/1918/head
Artur Arseniev 8 years ago
parent
commit
47b7af443d
  1. 2
      dist/css/grapes.min.css
  2. 13
      src/canvas/index.js
  3. 5
      src/canvas/view/CanvasView.js
  4. 1
      src/commands/index.js
  5. 299
      src/commands/view/ComponentDrag.js
  6. 90
      src/styles/scss/_gjs_canvas.scss
  7. 8
      src/styles/scss/main.scss
  8. 113
      src/utils/Dragger.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

13
src/canvas/index.js

@ -302,6 +302,19 @@ module.exports = () => {
return CanvasView.getElementOffsets(el);
},
/**
* Get canvas rectangular data
* @returns {Object}
*/
getRect() {
const { top, left } = CanvasView.getPosition();
return {
...CanvasView.getCanvasOffset(),
topScroll: top,
leftScroll: left
};
},
/**
* This method comes handy when you need to attach something like toolbars
* to elements inside the canvas, dealing with all relative position,

5
src/canvas/view/CanvasView.js

@ -73,7 +73,7 @@ module.exports = Backbone.View.extend({
}
},
updateFrames() {
updateFrames(ev) {
const { em, model } = this;
const { x, y } = model.attributes;
const zoom = this.getZoom();
@ -84,6 +84,7 @@ module.exports = Backbone.View.extend({
this.clearOff();
this.onFrameScroll();
em.stopDefault(defOpts);
em.trigger('canvas:update', ev);
timerZoom && clearTimeout(timerZoom);
timerZoom = setTimeout(() => em.runDefault(defOpts));
},
@ -353,7 +354,7 @@ module.exports = Backbone.View.extend({
const height = eo.height * zoom;
const width = eo.width * zoom;
return { top, left, height, width };
return { top, left, height, width, zoom, rect: eo };
},
/**

1
src/commands/index.js

@ -144,6 +144,7 @@ module.exports = () => {
if (includes(modes, mode)) {
// TODO move grabbing func in editor/canvas from the Sorter
dragger = editor.runCommand('core:component-drag', {
guidesInfo: 1,
mode,
target: sel,
onEnd,

299
src/commands/view/ComponentDrag.js

@ -1,10 +1,20 @@
import { keys, bindAll } from 'underscore';
import { keys, bindAll, each, isUndefined } from 'underscore';
import Dragger from 'utils/Dragger';
module.exports = {
run(editor, sender, opts = {}) {
bindAll(this, 'setPosition', 'onStart', 'onEnd', 'getPosition');
const { target, event, mode } = opts;
bindAll(
this,
'setPosition',
'onStart',
'onDrag',
'onEnd',
'getPosition',
'getGuidesStatic',
'renderGuide',
'getGuidesTarget'
);
const { target, event, mode, dragger = {} } = opts;
const { Canvas } = editor;
const el = target.getEl();
const scale = Canvas.getZoomMultiplier();
@ -13,32 +23,215 @@ module.exports = {
doc: el.ownerDocument,
onStart: this.onStart,
onEnd: this.onEnd,
onDrag: this.onDrag,
getPosition: this.getPosition,
setPosition: this.setPosition
setPosition: this.setPosition,
guidesStatic: () => this.guidesStatic,
guidesTarget: () => this.guidesTarget,
...dragger
};
this.setupGuides();
this.opts = opts;
this.editor = editor;
this.em = editor.getModel();
this.target = target;
this.isTran = mode == 'translate';
let dragger = this.dragger;
this.guidesContainer = this.getGuidesContainer();
this.guidesTarget = this.getGuidesTarget();
this.guidesStatic = this.getGuidesStatic();
window.guidesTarget = this.guidesTarget;
let drg = this.dragger;
if (!dragger) {
dragger = new Dragger(config);
this.dragger = dragger;
if (!drg) {
drg = new Dragger(config);
this.dragger = drg;
} else {
dragger.setOptions(config);
drg.setOptions(config);
}
event && dragger.start(event);
event && drg.start(event);
this.toggleDrag(1);
return dragger;
return drg;
},
stop() {
this.toggleDrag();
},
setupGuides() {
(this.guides || []).forEach(item => {
const { guide } = item;
guide && guide.parentNode.removeChild(guide);
});
this.guides = [];
},
getGuidesContainer() {
let { guidesEl } = this;
if (!guidesEl) {
const { editor, em, opts } = this;
const pfx = editor.getConfig('stylePrefix');
const elInfoX = document.createElement('div');
const elInfoY = document.createElement('div');
const guideContent = `<div class="${pfx}guide-info__line ${pfx}danger-bg">
<div class="${pfx}guide-info__content ${pfx}danger-color"></div>
</div>`;
guidesEl = document.createElement('div');
guidesEl.className = `${pfx}guides`;
elInfoX.className = `${pfx}guide-info ${pfx}guide-info__x`;
elInfoY.className = `${pfx}guide-info ${pfx}guide-info__y`;
elInfoX.innerHTML = guideContent;
elInfoY.innerHTML = guideContent;
guidesEl.appendChild(elInfoX);
guidesEl.appendChild(elInfoY);
editor.Canvas.getToolsEl().appendChild(guidesEl);
this.guidesEl = guidesEl;
this.elGuideInfoX = elInfoX;
this.elGuideInfoY = elInfoY;
this.elGuideInfoContentX = elInfoX.querySelector(
`.${pfx}guide-info__content`
);
this.elGuideInfoContentY = elInfoY.querySelector(
`.${pfx}guide-info__content`
);
em.on('canvas:update', () => {
this.updateGuides();
opts.debug && this.guides.forEach(item => this.renderGuide(item));
});
}
return guidesEl;
},
getGuidesStatic() {
let result = [];
const el = this.target.getEl();
const { parentNode = {} } = el;
each(
parentNode.children,
item =>
(result = result.concat(el !== item ? this.getElementGuides(item) : []))
);
return result.concat(this.getElementGuides(parentNode));
},
getGuidesTarget() {
return this.getElementGuides(this.target.getEl());
},
updateGuides(guides) {
(guides || this.guides).forEach(item => {
const { origin } = item;
const { top, height, left, width } = editor.Canvas.getElementPos(origin);
switch (item.type) {
case 't':
return (item.y = top);
case 'b':
return (item.y = top + height);
case 'l':
return (item.x = left);
case 'r':
return (item.x = left + width);
case 'x':
return (item.x = left + width / 2);
case 'y':
return (item.y = top + height / 2);
}
});
},
getGuidePosUpdate(item, rect) {
const result = {};
const { top, height, left, width } = rect;
switch (item.type) {
case 't':
result.y = top;
break;
case 'b':
result.y = top + height;
break;
case 'l':
result.x = left;
break;
case 'r':
result.x = left + width;
break;
case 'x':
result.x = left + width / 2;
break;
case 'y':
result.y = top + height / 2;
break;
}
return result;
},
renderGuide(item = {}) {
const el = item.guide || document.createElement('div');
const { Canvas } = this.editor;
const { topScroll, top } = Canvas.getRect();
const frameTop = Canvas.getCanvasView().getFrameOffset().top;
// const elRect = this.getGuidePosUpdate(item, el.getBoundingClientRect());
const un = 'px';
const guideSize = item.active ? 2 : 1;
let numEl = el.children[0];
el.style = `position: absolute; background-color: ${
item.active ? 'green' : 'red'
};`;
if (!el.children.length) {
numEl = document.createElement('div');
numEl.style =
'position: absolute; color: red; padding: 5px; top: 0; left: 0;';
el.appendChild(numEl);
}
if (item.y) {
el.style.width = '100%';
el.style.height = `${guideSize}${un}`;
el.style.top = `${item.y}${un}`;
el.style.left = 0;
// numEl.innerHTML = elRect.y;
} else {
el.style.width = `${guideSize}${un}`;
el.style.height = '100%';
el.style.left = `${item.x}${un}`;
el.style.top = `${topScroll - frameTop + top}${un}`;
// numEl.innerHTML = elRect.x;
// numEl.innerHTML = el.style.left;
}
!item.guide && this.guidesContainer.appendChild(el);
return el;
},
getElementGuides(el) {
const { editor, opts } = this;
const { top, height, left, width } = editor.Canvas.getElementPos(el);
const guides = [
{ type: 't', y: top }, // Top
{ type: 'b', y: top + height }, // Bottom
{ type: 'l', x: left }, // Left
{ type: 'r', x: left + width }, // Right
{ type: 'x', x: left + width / 2 }, // Mid x
{ type: 'y', y: top + height / 2 } // Mid y
].map(item => ({
...item,
origin: el,
originRect: editor.Canvas.getElementPos(el),
guide: opts.debug && this.renderGuide(item)
}));
guides.forEach(item => this.guides.push(item));
return guides;
},
getTranslate(transform, axis = 'x') {
let result = 0;
(transform || '').split(' ').forEach(item => {
@ -117,11 +310,95 @@ module.exports = {
}
},
onDrag() {
const { guidesTarget, opts } = this;
this.updateGuides(guidesTarget);
opts.debug && guidesTarget.forEach(item => this.renderGuide(item));
opts.guidesInfo &&
this.renderGuideInfo(guidesTarget.filter(item => item.active));
},
onEnd() {
const { editor, opts, id } = this;
const { onEnd } = opts;
onEnd && onEnd();
editor.stopCommand(id);
this.hideGuidesInfo();
},
hideGuidesInfo() {
['X', 'Y'].forEach(item => {
const guide = this[`elGuideInfo${item}`];
if (guide) guide.style.display = 'none';
});
},
/**
* Render guides with spacing information
*/
renderGuideInfo(guides = []) {
const { guidesStatic, editor } = this;
this.hideGuidesInfo();
guides.forEach(item => {
const { origin, x } = item;
const rectOrigin = editor.Canvas.getElementPos(origin);
const axis = isUndefined(x) ? 'y' : 'x';
const isY = axis === 'y';
const origEdge1 = rectOrigin[isY ? 'left' : 'top'];
const origEdge1Raw = rectOrigin.rect[isY ? 'left' : 'top'];
const origEdge2 = isY
? origEdge1 + rectOrigin.width
: origEdge1 + rectOrigin.height;
const origEdge2Raw = isY
? origEdge1Raw + rectOrigin.rect.width
: origEdge1Raw + rectOrigin.rect.height;
const elGuideInfo = this[`elGuideInfo${axis.toUpperCase()}`];
const elGuideInfoCnt = this[`elGuideInfoContent${axis.toUpperCase()}`];
const guideInfoStyle = elGuideInfo.style;
// Find the nearest element
const res = guidesStatic
.filter(stat => stat[axis] === item[axis])
.map(stat => {
const { left, width, top, height } = stat.originRect;
const statEdge1 = isY ? left : top;
const statEdge2 = isY ? left + width : top + height;
return {
gap:
statEdge2 < origEdge1
? origEdge1 - statEdge2
: statEdge1 - origEdge2,
guide: stat
};
})
.filter(item => item.gap > 0)
.sort((a, b) => a.gap - b.gap)
.map(item => item.guide)[0];
if (res) {
const { left, width, top, height, rect } = res.originRect;
const isEdge1 = isY ? left < rectOrigin.left : top < rectOrigin.top;
const statEdge1 = isY ? left : top;
const statEdge1Raw = isY ? rect.left : rect.top;
const statEdge2 = isY ? left + width : top + height;
const statEdge2Raw = isY
? rect.left + rect.width
: rect.top + rect.height;
const pos2 = `${isY ? item.y : item.x}px`;
const size = isEdge1 ? origEdge1 - statEdge2 : statEdge1 - origEdge2;
const sizeRaw = isEdge1
? origEdge1Raw - statEdge2Raw
: statEdge1Raw - origEdge2Raw;
guideInfoStyle.display = '';
guideInfoStyle[isY ? 'top' : 'left'] = pos2;
guideInfoStyle[isY ? 'left' : 'top'] = `${
isEdge1 ? statEdge2 : origEdge2
}px`;
guideInfoStyle[isY ? 'width' : 'height'] = `${size}px`;
elGuideInfoCnt.innerHTML = `${Math.round(sizeRaw)}px`;
}
});
},
toggleDrag(on) {

90
src/styles/scss/_gjs_canvas.scss

@ -1,5 +1,94 @@
$frameAnimation: 0.35s ease;
$canvasTop: 40px;
$guide_pad: 5px;
.#{$prefix} {
&guide-info {
position: absolute;
&__content {
position: absolute;
height: 100%;
display: flex;
width: 100%;
padding: 5px;
}
&__line {
position: relative;
margin: auto;
&::before,
&::after {
content: "";
display: block;
position: absolute;
background-color: inherit;
}
}
&__y {
padding: 0 $guide_pad;
.#{$prefix}guide-info {
&__content {
justify-content: center;
}
&__line {
width: 100%;
height: 1px;
&::before,
&::after {
width: 1px;
height: 10px;
top: 0;
bottom: 0;
left: 0;
margin: auto;
}
&::after {
left: auto;
right: 0;
}
}
}
}
&__x {
padding: $guide_pad 0;
.#{$prefix}guide-info {
&__content {
align-items: center;
}
&__line {
height: 100%;
width: 1px;
&::before,
&::after {
width: 10px;
height: 1px;
left: 0;
right: 0;
top: 0;
margin: auto;
transform: translateX(-50%);
}
&::after {
top: auto;
bottom: 0;
}
}
}
}
}
}
.#{$cv-prefix}canvas {
background-color: rgba(0, 0, 0, 0.15);
@ -57,6 +146,7 @@ $canvasTop: 40px;
##{$app-prefix}tools {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;

8
src/styles/scss/main.scss

@ -52,8 +52,14 @@ $prefix: $app-prefix;
@import "gjs_status";
$colorsAll: (one, $primaryColor),
(two, $secondaryColor),
(three, $tertiaryColor),
(four, $quaternaryColor),
(danger, $colorRed);
.#{$prefix} {
@each $cnum, $ccol in (one, $primaryColor), (two, $secondaryColor), (three, $tertiaryColor), (four, $quaternaryColor) {
@each $cnum, $ccol in $colorsAll {
&#{$cnum} {
&-bg {
background-color: $ccol;

113
src/utils/Dragger.js

@ -1,4 +1,4 @@
import { bindAll, isFunction, result } from 'underscore';
import { bindAll, isFunction, result, isUndefined } from 'underscore';
import { on, off } from 'utils/mixins';
export default class Dragger {
@ -42,6 +42,15 @@ export default class Dragger {
*/
getPosition: null,
// Static guides to be snapped
guidesStatic: null,
// Target guides that will snap to static one
guidesTarget: null,
// Offset before snap to guides
snapOffset: 5,
// Document on which listen to pointer events
doc: 0,
@ -78,11 +87,14 @@ export default class Dragger {
* @param {Event} e
*/
start(ev) {
const { onStart } = this.opts;
const { opts } = this;
const { onStart } = opts;
this.toggleDrag(1);
this.startPointer = this.getPointerPos(ev);
isFunction(onStart) && onStart(ev, this);
this.startPosition = this.getStartPosition();
this.guidesStatic = result(opts, 'guidesStatic') || [];
this.guidesTarget = result(opts, 'guidesTarget') || [];
this.drag(ev);
}
@ -114,17 +126,104 @@ export default class Dragger {
delta.y = startPointer.y;
}
['x', 'y'].forEach(co => (delta[co] = delta[co] * result(opts, 'scale')));
this.lockedAxis = lockedAxis;
this.delta = delta;
this.move(delta.x, delta.y);
const moveDelta = delta => {
['x', 'y'].forEach(co => (delta[co] = delta[co] * result(opts, 'scale')));
this.delta = delta;
this.move(delta.x, delta.y);
isFunction(onDrag) && onDrag(ev, this);
};
const deltaPre = { ...delta };
this.currentPointer = currentPos;
isFunction(onDrag) && onDrag(ev, this);
this.lockedAxis = lockedAxis;
moveDelta(delta);
if (this.guidesTarget.length) {
const { newDelta, trgX, trgY } = this.snapGuides(deltaPre);
(trgX || trgY) && moveDelta(newDelta);
}
// In case the mouse button was released outside of the window
ev.which === 0 && this.stop(ev);
}
/**
* Check if the delta hits some guide
*/
snapGuides(delta) {
const newDelta = delta;
let { trgX, trgY } = this;
this.guidesTarget.forEach(trg => {
// Skip the guide if its locked axis already exists
if ((trg.x && this.trgX) || (trg.y && this.trgY)) return;
trg.active = 0;
this.guidesStatic.forEach(stat => {
if ((trg.y && stat.x) || (trg.x && stat.y)) return;
const isY = trg.y && stat.y;
const axs = isY ? 'y' : 'x';
const trgPoint = trg[axs];
const statPoint = stat[axs];
const deltaPoint = delta[axs];
const trgGuide = isY ? trgY : trgX;
if (this.isPointIn(trgPoint, statPoint)) {
if (isUndefined(trgGuide)) {
const trgValue = deltaPoint - (trgPoint - statPoint);
this.setGuideLock(trg, trgValue);
}
}
});
});
trgX = this.trgX;
trgY = this.trgY;
['x', 'y'].forEach(co => {
const axis = co.toUpperCase();
let trg = this[`trg${axis}`];
if (trg && !this.isPointIn(delta[co], trg.lock)) {
this.setGuideLock(trg, null);
trg = null;
}
if (trg && !isUndefined(trg.lock)) {
newDelta[co] = trg.lock;
}
});
return {
newDelta,
trgX: this.trgX,
trgY: this.trgY
};
}
isPointIn(src, trg, { offset } = {}) {
const ofst = offset || this.opts.snapOffset;
return (
(src >= trg && src <= trg + ofst) || (src <= trg && src >= trg - ofst)
);
}
setGuideLock(guide, value) {
const axis = !isUndefined(guide.x) ? 'X' : 'Y';
const trgName = `trg${axis}`;
if (value !== null) {
guide.active = 1;
guide.lock = value;
this[trgName] = guide;
} else {
delete guide.active;
delete guide.lock;
delete this[trgName];
}
return guide;
}
/**
* Stop dragging
*/

Loading…
Cancel
Save