devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
406 lines (405 loc) • 15.8 kB
JavaScript
/**
* DevExtreme (esm/ui/resizable.js)
* Version: 21.1.4
* Build date: Mon Jun 21 2021
*
* Copyright (c) 2012 - 2021 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
import {
locate,
move
} from "../animation/translator";
import registerComponent from "../core/component_registrator";
import DOMComponent from "../core/dom_component";
import $ from "../core/renderer";
import {
inArray
} from "../core/utils/array";
import {
pairToObject
} from "../core/utils/common";
import {
extend
} from "../core/utils/extend";
import {
each
} from "../core/utils/iterator";
import {
fitIntoRange
} from "../core/utils/math";
import {
isPlainObject,
isFunction,
isWindow
} from "../core/utils/type";
import {
hasWindow
} from "../core/utils/window";
import eventsEngine from "../events/core/events_engine";
import {
start as dragEventStart,
move as dragEventMove,
end as dragEventEnd
} from "../events/drag";
import {
getBoundingRect
} from "../core/utils/position";
import {
addNamespace
} from "../events/utils/index";
import {
triggerResizeEvent
} from "../events/visibility_change";
var RESIZABLE = "dxResizable";
var RESIZABLE_CLASS = "dx-resizable";
var RESIZABLE_RESIZING_CLASS = "dx-resizable-resizing";
var RESIZABLE_HANDLE_CLASS = "dx-resizable-handle";
var RESIZABLE_HANDLE_TOP_CLASS = "dx-resizable-handle-top";
var RESIZABLE_HANDLE_BOTTOM_CLASS = "dx-resizable-handle-bottom";
var RESIZABLE_HANDLE_LEFT_CLASS = "dx-resizable-handle-left";
var RESIZABLE_HANDLE_RIGHT_CLASS = "dx-resizable-handle-right";
var RESIZABLE_HANDLE_CORNER_CLASS = "dx-resizable-handle-corner";
var DRAGSTART_START_EVENT_NAME = addNamespace(dragEventStart, RESIZABLE);
var DRAGSTART_EVENT_NAME = addNamespace(dragEventMove, RESIZABLE);
var DRAGSTART_END_EVENT_NAME = addNamespace(dragEventEnd, RESIZABLE);
var SIDE_BORDER_WIDTH_STYLES = {
left: "borderLeftWidth",
top: "borderTopWidth",
right: "borderRightWidth",
bottom: "borderBottomWidth"
};
var Resizable = DOMComponent.inherit({
_getDefaultOptions: function() {
return extend(this.callBase(), {
handles: "all",
step: "1",
stepPrecision: "simple",
area: void 0,
minWidth: 30,
maxWidth: 1 / 0,
minHeight: 30,
maxHeight: 1 / 0,
onResizeStart: null,
onResize: null,
onResizeEnd: null,
roundStepValue: true
})
},
_init: function() {
this.callBase();
this.$element().addClass(RESIZABLE_CLASS)
},
_initMarkup: function() {
this.callBase();
this._renderHandles()
},
_render: function() {
this.callBase();
this._renderActions()
},
_renderActions: function() {
this._resizeStartAction = this._createActionByOption("onResizeStart");
this._resizeEndAction = this._createActionByOption("onResizeEnd");
this._resizeAction = this._createActionByOption("onResize")
},
_renderHandles: function() {
this._handles = [];
var handles = this.option("handles");
if ("none" === handles) {
return
}
var directions = "all" === handles ? ["top", "bottom", "left", "right"] : handles.split(" ");
each(directions, (index, handleName) => {
this._renderHandle(handleName)
});
inArray("bottom", directions) + 1 && inArray("right", directions) + 1 && this._renderHandle("corner-bottom-right");
inArray("bottom", directions) + 1 && inArray("left", directions) + 1 && this._renderHandle("corner-bottom-left");
inArray("top", directions) + 1 && inArray("right", directions) + 1 && this._renderHandle("corner-top-right");
inArray("top", directions) + 1 && inArray("left", directions) + 1 && this._renderHandle("corner-top-left");
this._attachEventHandlers()
},
_renderHandle: function(handleName) {
var $handle = $("<div>").addClass(RESIZABLE_HANDLE_CLASS).addClass(RESIZABLE_HANDLE_CLASS + "-" + handleName).appendTo(this.$element());
this._handles.push($handle)
},
_attachEventHandlers: function() {
if (this.option("disabled")) {
return
}
var handlers = {};
handlers[DRAGSTART_START_EVENT_NAME] = this._dragStartHandler.bind(this);
handlers[DRAGSTART_EVENT_NAME] = this._dragHandler.bind(this);
handlers[DRAGSTART_END_EVENT_NAME] = this._dragEndHandler.bind(this);
this._handles.forEach(handleElement => {
eventsEngine.on(handleElement, handlers, {
direction: "both",
immediate: true
})
})
},
_detachEventHandlers: function() {
this._handles.forEach(handleElement => {
eventsEngine.off(handleElement)
})
},
_toggleEventHandlers: function(shouldAttachEvents) {
shouldAttachEvents ? this._attachEventHandlers() : this._detachEventHandlers()
},
_dragStartHandler: function(e) {
var $element = this.$element();
if ($element.is(".dx-state-disabled, .dx-state-disabled *")) {
e.cancel = true;
return
}
this._toggleResizingClass(true);
this._movingSides = this._getMovingSides(e);
this._elementLocation = locate($element);
var elementRect = getBoundingRect($element.get(0));
this._elementSize = {
width: elementRect.width,
height: elementRect.height
};
this._renderDragOffsets(e);
this._resizeStartAction({
event: e,
width: this._elementSize.width,
height: this._elementSize.height,
handles: this._movingSides
});
e.targetElements = null
},
_toggleResizingClass: function(value) {
this.$element().toggleClass(RESIZABLE_RESIZING_CLASS, value)
},
_renderDragOffsets: function(e) {
var area = this._getArea();
if (!area) {
return
}
var $handle = $(e.target).closest("." + RESIZABLE_HANDLE_CLASS);
var handleWidth = $handle.outerWidth();
var handleHeight = $handle.outerHeight();
var handleOffset = $handle.offset();
var areaOffset = area.offset;
var scrollOffset = this._getAreaScrollOffset();
e.maxLeftOffset = handleOffset.left - areaOffset.left - scrollOffset.scrollX;
e.maxRightOffset = areaOffset.left + area.width - handleOffset.left - handleWidth + scrollOffset.scrollX;
e.maxTopOffset = handleOffset.top - areaOffset.top - scrollOffset.scrollY;
e.maxBottomOffset = areaOffset.top + area.height - handleOffset.top - handleHeight + scrollOffset.scrollY
},
_getBorderWidth: function($element, direction) {
if (isWindow($element.get(0))) {
return 0
}
var borderWidth = $element.css(SIDE_BORDER_WIDTH_STYLES[direction]);
return parseInt(borderWidth) || 0
},
_dragHandler: function(e) {
var $element = this.$element();
var sides = this._movingSides;
var location = this._elementLocation;
var size = this._elementSize;
var offset = this._getOffset(e);
var width = size.width + offset.x * (sides.left ? -1 : 1);
var height = size.height + offset.y * (sides.top ? -1 : 1);
if (offset.x || "strict" === this.option("stepPrecision")) {
this._renderWidth(width)
}
if (offset.y || "strict" === this.option("stepPrecision")) {
this._renderHeight(height)
}
var elementRect = getBoundingRect($element.get(0));
var offsetTop = offset.y - ((elementRect.height || height) - height);
var offsetLeft = offset.x - ((elementRect.width || width) - width);
move($element, {
top: location.top + (sides.top ? offsetTop : 0),
left: location.left + (sides.left ? offsetLeft : 0)
});
this._resizeAction({
event: e,
width: this.option("width") || width,
height: this.option("height") || height,
handles: this._movingSides
});
triggerResizeEvent($element)
},
_getOffset: function(e) {
var offset = e.offset;
var steps = pairToObject(this.option("step"), !this.option("roundStepValue"));
var sides = this._getMovingSides(e);
var strictPrecision = "strict" === this.option("stepPrecision");
if (!sides.left && !sides.right) {
offset.x = 0
}
if (!sides.top && !sides.bottom) {
offset.y = 0
}
return strictPrecision ? this._getStrictOffset(offset, steps, sides) : this._getSimpleOffset(offset, steps)
},
_getSimpleOffset: function(offset, steps) {
return {
x: offset.x - offset.x % steps.h,
y: offset.y - offset.y % steps.v
}
},
_getStrictOffset: function(offset, steps, sides) {
var location = this._elementLocation;
var size = this._elementSize;
var xPos = sides.left ? location.left : location.left + size.width;
var yPos = sides.top ? location.top : location.top + size.height;
var newXShift = (xPos + offset.x) % steps.h;
var newYShift = (yPos + offset.y) % steps.v;
var sign = Math.sign || (x => {
x = +x;
if (0 === x || isNaN(x)) {
return x
}
return x > 0 ? 1 : -1
});
var separatorOffset = (steps, offset) => (1 + .2 * sign(offset)) % 1 * steps;
var isSmallOffset = (offset, steps) => Math.abs(offset) < .2 * steps;
var newOffsetX = offset.x - newXShift;
var newOffsetY = offset.y - newYShift;
if (newXShift > separatorOffset(steps.h, offset.x)) {
newOffsetX += steps.h
}
if (newYShift > separatorOffset(steps.v, offset.y)) {
newOffsetY += steps.v
}
return {
x: (sides.left || sides.right) && !isSmallOffset(offset.x, steps.h) ? newOffsetX : 0,
y: (sides.top || sides.bottom) && !isSmallOffset(offset.y, steps.v) ? newOffsetY : 0
}
},
_getMovingSides: function(e) {
var $target = $(e.target);
var hasCornerTopLeftClass = $target.hasClass(RESIZABLE_HANDLE_CORNER_CLASS + "-top-left");
var hasCornerTopRightClass = $target.hasClass(RESIZABLE_HANDLE_CORNER_CLASS + "-top-right");
var hasCornerBottomLeftClass = $target.hasClass(RESIZABLE_HANDLE_CORNER_CLASS + "-bottom-left");
var hasCornerBottomRightClass = $target.hasClass(RESIZABLE_HANDLE_CORNER_CLASS + "-bottom-right");
return {
top: $target.hasClass(RESIZABLE_HANDLE_TOP_CLASS) || hasCornerTopLeftClass || hasCornerTopRightClass,
left: $target.hasClass(RESIZABLE_HANDLE_LEFT_CLASS) || hasCornerTopLeftClass || hasCornerBottomLeftClass,
bottom: $target.hasClass(RESIZABLE_HANDLE_BOTTOM_CLASS) || hasCornerBottomLeftClass || hasCornerBottomRightClass,
right: $target.hasClass(RESIZABLE_HANDLE_RIGHT_CLASS) || hasCornerTopRightClass || hasCornerBottomRightClass
}
},
_getArea: function() {
var area = this.option("area");
if (isFunction(area)) {
area = area.call(this)
}
if (isPlainObject(area)) {
return this._getAreaFromObject(area)
}
return this._getAreaFromElement(area)
},
_getAreaScrollOffset: function() {
var area = this.option("area");
var isElement = !isFunction(area) && !isPlainObject(area);
var scrollOffset = {
scrollY: 0,
scrollX: 0
};
if (isElement) {
var areaElement = $(area)[0];
if (isWindow(areaElement)) {
scrollOffset.scrollX = areaElement.pageXOffset;
scrollOffset.scrollY = areaElement.pageYOffset
}
}
return scrollOffset
},
_getAreaFromObject: function(area) {
var result = {
width: area.right - area.left,
height: area.bottom - area.top,
offset: {
left: area.left,
top: area.top
}
};
this._correctAreaGeometry(result);
return result
},
_getAreaFromElement: function(area) {
var $area = $(area);
var result;
if ($area.length) {
result = {
width: $area.innerWidth(),
height: $area.innerHeight(),
offset: extend({
top: 0,
left: 0
}, isWindow($area[0]) ? {} : $area.offset())
};
this._correctAreaGeometry(result, $area)
}
return result
},
_correctAreaGeometry: function(result, $area) {
var areaBorderLeft = $area ? this._getBorderWidth($area, "left") : 0;
var areaBorderTop = $area ? this._getBorderWidth($area, "top") : 0;
result.offset.left += areaBorderLeft + this._getBorderWidth(this.$element(), "left");
result.offset.top += areaBorderTop + this._getBorderWidth(this.$element(), "top");
result.width -= this.$element().outerWidth() - this.$element().innerWidth();
result.height -= this.$element().outerHeight() - this.$element().innerHeight()
},
_dragEndHandler: function(e) {
var $element = this.$element();
this._resizeEndAction({
event: e,
width: $element.outerWidth(),
height: $element.outerHeight(),
handles: this._movingSides
});
this._toggleResizingClass(false)
},
_renderWidth: function(width) {
this.option("width", fitIntoRange(width, this.option("minWidth"), this.option("maxWidth")))
},
_renderHeight: function(height) {
this.option("height", fitIntoRange(height, this.option("minHeight"), this.option("maxHeight")))
},
_optionChanged: function(args) {
switch (args.name) {
case "disabled":
this._toggleEventHandlers(!args.value);
this.callBase(args);
break;
case "handles":
this._invalidate();
break;
case "minWidth":
case "maxWidth":
hasWindow() && this._renderWidth(this.$element().outerWidth());
break;
case "minHeight":
case "maxHeight":
hasWindow() && this._renderHeight(this.$element().outerHeight());
break;
case "onResize":
case "onResizeStart":
case "onResizeEnd":
this._renderActions();
break;
case "area":
case "stepPrecision":
case "step":
case "roundStepValue":
break;
default:
this.callBase(args)
}
},
_clean: function() {
this.$element().find("." + RESIZABLE_HANDLE_CLASS).remove()
},
_useTemplates: function() {
return false
}
});
registerComponent(RESIZABLE, Resizable);
export default Resizable;