grapesjs_codeapps
Version:
Free and Open Source Web Builder Framework/SC Modification
505 lines (441 loc) • 12.1 kB
JavaScript
import { bindAll, defaults, isFunction, each } from 'underscore';
import { on, off, normalizeFloat } from 'utils/mixins';
var defaultOpts = {
// Function which returns custom X and Y coordinates of the mouse
mousePosFetcher: null,
// Indicates custom target updating strategy
updateTarget: null,
// Function which gets HTMLElement as an arg and returns it relative position
ratioDefault: 0,
posFetcher: null,
onStart: null,
onMove: null,
onEnd: null,
// Resize unit step
step: 1,
// Minimum dimension
minDim: 32,
// Maximum dimension
maxDim: '',
// Unit used for height resizing
unitHeight: 'px',
// Unit used for width resizing
unitWidth: 'px',
// The key used for height resizing
keyHeight: 'height',
// The key used for width resizing
keyWidth: 'width',
// If true, will override unitHeight and unitWidth, on start, with units
// from the current focused element (currently used only in SelectComponent)
currentUnit: 1,
// With this option active the mousemove event won't be altered when
// the pointer comes over iframes
silentFrames: 0,
// If true the container of handlers won't be updated
avoidContainerUpdate: 0,
// If height is 'auto', this setting will preserve it and only update width
keepAutoHeight: false,
// If width is 'auto', this setting will preserve it and only update height
keepAutoWidth: false,
// When keepAutoHeight is true and the height has the value 'auto', this is set to true and height isn't updated
autoHeight: false,
// When keepAutoWidth is true and the width has the value 'auto', this is set to true and width isn't updated
autoWidth: false,
// Handlers
tl: 1, // Top left
tc: 1, // Top center
tr: 1, // Top right
cl: 1, // Center left
cr: 1, // Center right
bl: 1, // Bottom left
bc: 1, // Bottom center
br: 1 // Bottom right
};
var createHandler = (name, opts) => {
var pfx = opts.prefix || '';
var el = document.createElement('i');
el.className = pfx + 'resizer-h ' + pfx + 'resizer-h-' + name;
el.setAttribute('data-' + pfx + 'handler', name);
return el;
};
var getBoundingRect = (el, win) => {
var w = win || window;
var rect = el.getBoundingClientRect();
return {
left: rect.left + w.pageXOffset,
top: rect.top + w.pageYOffset,
width: rect.width,
height: rect.height
};
};
class Resizer {
/**
* Init the Resizer with options
* @param {Object} options
*/
constructor(opts = {}) {
this.setOptions(opts);
bindAll(this, 'handleKeyDown', 'handleMouseDown', 'move', 'stop');
return this;
}
/**
* Get current connfiguration options
* @return {Object}
*/
getConfig() {
return this.opts;
}
/**
* Setup options
* @param {Object} options
*/
setOptions(options = {}) {
this.opts = defaults(options, defaultOpts);
this.setup();
}
/**
* Setup resizer
*/
setup() {
const opts = this.opts;
const pfx = opts.prefix || '';
const appendTo = opts.appendTo || document.body;
let container = this.container;
// Create container if not yet exist
if (!container) {
container = document.createElement('div');
container.className = `${pfx}resizer-c`;
appendTo.appendChild(container);
this.container = container;
}
while (container.firstChild) {
container.removeChild(container.firstChild);
}
// Create handlers
const handlers = {};
['tl', 'tc', 'tr', 'cl', 'cr', 'bl', 'bc', 'br'].forEach(
hdl => (handlers[hdl] = opts[hdl] ? createHandler(hdl, opts) : '')
);
for (let n in handlers) {
const handler = handlers[n];
handler && container.appendChild(handler);
}
this.handlers = handlers;
this.mousePosFetcher = opts.mousePosFetcher;
this.updateTarget = opts.updateTarget;
this.posFetcher = opts.posFetcher;
this.onStart = opts.onStart;
this.onMove = opts.onMove;
this.onEnd = opts.onEnd;
}
/**
* Toggle iframes pointer event
* @param {Boolean} silent If true, iframes will be silented
*/
toggleFrames(silent) {
if (this.opts.silentFrames) {
const frames = document.querySelectorAll('iframe');
each(frames, frame => (frame.style.pointerEvents = silent ? 'none' : ''));
}
}
/**
* Detects if the passed element is a resize handler
* @param {HTMLElement} el
* @return {Boolean}
*/
isHandler(el) {
var handlers = this.handlers;
for (var n in handlers) {
if (handlers[n] === el) return true;
}
return false;
}
/**
* Returns the focused element
* @return {HTMLElement}
*/
getFocusedEl() {
return this.el;
}
/**
* Returns documents
*/
getDocumentEl() {
return [this.el.ownerDocument, document];
}
/**
* Return element position
* @param {HTMLElement} el
* @param {Object} opts Custom options
* @return {Object}
*/
getElementPos(el, opts = {}) {
var posFetcher = this.posFetcher || '';
return posFetcher ? posFetcher(el, opts) : getBoundingRect(el);
}
/**
* Focus resizer on the element, attaches handlers to it
* @param {HTMLElement} el
*/
focus(el) {
// Avoid focusing on already focused element
if (el && el === this.el) {
return;
}
// Show the handlers
this.el = el;
const config = this.opts;
const unit = 'px';
const rect = this.getElementPos(el, { target: 'container' });
const container = this.container;
const contStyle = container.style;
if (!config.avoidContainerUpdate) {
contStyle.left = rect.left + unit;
contStyle.top = rect.top + unit;
contStyle.width = rect.width + unit;
contStyle.height = rect.height + unit;
contStyle.display = 'block';
}
on(this.getDocumentEl(), 'mousedown', this.handleMouseDown);
}
/**
* Blur from element
*/
blur() {
this.container.style.display = 'none';
if (this.el) {
off(this.getDocumentEl(), 'mousedown', this.handleMouseDown);
this.el = null;
}
}
/**
* Start resizing
* @param {Event} e
*/
start(e) {
//Right or middel click
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
const el = this.el;
const resizer = this;
const config = this.opts || {};
var attrName = 'data-' + config.prefix + 'handler';
var rect = this.getElementPos(el, { target: 'el' });
this.handlerAttr = e.target.getAttribute(attrName);
this.clickedHandler = e.target;
this.startDim = {
t: rect.top,
l: rect.left,
w: rect.width,
h: rect.height
};
this.rectDim = {
t: rect.top,
l: rect.left,
w: rect.width,
h: rect.height
};
this.startPos = {
x: e.clientX,
y: e.clientY
};
// Listen events
var doc = this.getDocumentEl();
on(doc, 'mousemove', this.move);
on(doc, 'keydown', this.handleKeyDown);
on(doc, 'mouseup', this.stop);
isFunction(this.onStart) &&
this.onStart(e, { docs: doc, config, el, resizer });
this.toggleFrames(1);
this.move(e);
}
/**
* While resizing
* @param {Event} e
*/
move(e) {
const onMove = this.onMove;
var mouseFetch = this.mousePosFetcher;
var currentPos = mouseFetch
? mouseFetch(e)
: {
x: e.clientX,
y: e.clientY
};
this.currentPos = currentPos;
this.delta = {
x: currentPos.x - this.startPos.x,
y: currentPos.y - this.startPos.y
};
this.keys = {
shift: e.shiftKey,
ctrl: e.ctrlKey,
alt: e.altKey
};
this.rectDim = this.calc(this);
this.updateRect(0);
// Move callback
onMove && onMove(e);
// In case the mouse button was released outside of the window
if (e.which === 0) {
this.stop(e);
}
}
/**
* Stop resizing
* @param {Event} e
*/
stop(e) {
const config = this.opts;
var doc = this.getDocumentEl();
off(doc, 'mousemove', this.move);
off(doc, 'keydown', this.handleKeyDown);
off(doc, 'mouseup', this.stop);
this.updateRect(1);
this.toggleFrames();
isFunction(this.onEnd) && this.onEnd(e, { docs: doc, config });
}
/**
* Update rect
*/
updateRect(store) {
const el = this.el;
const resizer = this;
const config = this.opts;
const rect = this.rectDim;
const conStyle = this.container.style;
const updateTarget = this.updateTarget;
const selectedHandler = this.getSelectedHandler();
const { unitHeight, unitWidth, keyWidth, keyHeight } = config;
// Use custom updating strategy if requested
if (isFunction(updateTarget)) {
updateTarget(el, rect, {
store,
selectedHandler,
resizer,
config
});
} else {
const elStyle = el.style;
elStyle[keyWidth] = rect.w + unitWidth;
elStyle[keyHeight] = rect.h + unitHeight;
}
const unitRect = 'px';
const rectEl = this.getElementPos(el, { target: 'container' });
if (!config.avoidContainerUpdate) {
conStyle.left = rectEl.left + unitRect;
conStyle.top = rectEl.top + unitRect;
conStyle.width = rectEl.width + unitRect;
conStyle.height = rectEl.height + unitRect;
}
}
/**
* Get selected handler name
* @return {string}
*/
getSelectedHandler() {
var handlers = this.handlers;
if (!this.selectedHandler) {
return;
}
for (let n in handlers) {
if (handlers[n] === this.selectedHandler) return n;
}
}
/**
* Handle ESC key
* @param {Event} e
*/
handleKeyDown(e) {
if (e.keyCode === 27) {
// Rollback to initial dimensions
this.rectDim = this.startDim;
this.stop(e);
}
}
/**
* Handle mousedown to check if it's possible to start resizing
* @param {Event} e
*/
handleMouseDown(e) {
var el = e.target;
if (this.isHandler(el)) {
this.selectedHandler = el;
this.start(e);
} else if (el !== this.el) {
this.selectedHandler = '';
this.blur();
}
}
/**
* All positioning logic
* @return {Object}
*/
calc(data) {
let value;
const opts = this.opts || {};
const step = opts.step;
const startDim = this.startDim;
const minDim = opts.minDim;
const maxDim = opts.maxDim;
const deltaX = data.delta.x;
const deltaY = data.delta.y;
const startW = startDim.w;
const startH = startDim.h;
var box = {
t: 0,
l: 0,
w: startW,
h: startH
};
if (!data) return;
var attr = data.handlerAttr;
if (~attr.indexOf('r')) {
value = normalizeFloat(startW + deltaX * step, step);
value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value));
box.w = value;
}
if (~attr.indexOf('b')) {
value = normalizeFloat(startH + deltaY * step, step);
value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value));
box.h = value;
}
if (~attr.indexOf('l')) {
value = normalizeFloat(startW - deltaX * step, step);
value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value));
box.w = value;
}
if (~attr.indexOf('t')) {
value = normalizeFloat(startH - deltaY * step, step);
value = Math.max(minDim, value);
maxDim && (value = Math.min(maxDim, value));
box.h = value;
}
// Enforce aspect ratio (unless shift key is being held)
var ratioActive = opts.ratioDefault ? !data.keys.shift : data.keys.shift;
if (attr.indexOf('c') < 0 && ratioActive) {
var ratio = startDim.w / startDim.h;
if (box.w / box.h > ratio) {
box.h = Math.round(box.w / ratio);
} else {
box.w = Math.round(box.h * ratio);
}
}
if (~attr.indexOf('l')) {
box.l = startDim.w - box.w;
}
if (~attr.indexOf('t')) {
box.t = startDim.h - box.h;
}
return box;
}
}
module.exports = {
init(opts) {
return new Resizer(opts);
}
};