@cocreate/resize
Version:
A light weight resize handler make any element resizable by adding data attributes or usinng init object. Great for Customized text box or text area.
717 lines (643 loc) • 18.5 kB
JavaScript
// "use strict";
import observer from "@cocreate/observer";
import "./index.css";
const EVENTS = [
"mousemove touchmove",
"mousedown touchstart",
"mouseup touchend"
];
const DIRECTIONS = ["left", "right", "top", "bottom"];
const styleEl = document.createElement("style");
styleEl.setAttribute("component", "resize");
document.head.appendChild(styleEl);
const styleSheet = styleEl.sheet;
const coCreateResize = {
selector: "", //'.resize',
resizers: [],
resizeWidgets: [],
init: function (handleObj) {
for (var handleKey in handleObj)
if (handleObj.hasOwnProperty(handleKey) && handleKey == "selector")
this.selector = handleObj[handleKey];
this.resizers = document.querySelectorAll(this.selector);
var _this = this;
this.resizers.forEach(function (resize, idx) {
let resizeWidget = new CoCreateResize(resize, handleObj);
_this.resizeWidgets[idx] = resizeWidget;
});
},
initElement: function (target) {
let resizeWidget = new CoCreateResize(target, {
dragLeft: "[resize='left']",
dragRight: "[resize='right']",
dragTop: "[resize='top']",
dragBottom: "[resize='bottom']"
});
this.resizeWidgets[0] = resizeWidget;
}
};
function CoCreateResize(resizer, options) {
this.resizeWidget = resizer;
this.cornerSize = 10;
this.init(options);
}
CoCreateResize.prototype = {
init: function (handleObj) {
if (this.resizeWidget) {
this.initDrags = [];
this.checkCorners = [];
this.doDrags = [];
this.Drags = [];
//this is for grid
this.isGrid = this.detectGrid();
this.gridWidth = 0;
this.gridHeight = 0;
this.limitSpan = 0;
this.widthSpan = 0;
this.heightSpan = 0;
this.originClassAttribute = this.resizeWidget.getAttribute("class");
DIRECTIONS.map((d) => {
this.Drags[d] = this.resizeWidget.querySelector(
handleObj["drag" + d.charAt(0).toUpperCase() + d.slice(1)]
);
});
this.bindListeners();
this.initResize();
//this is just for grid system
this.getGridProperty();
window.addEventListener("resize", this.checkGridColumns.bind(this));
}
},
getGridProperty: function () {
if (!this.resizeWidget.parentNode) return;
let compStyles = window.getComputedStyle(this.resizeWidget.parentNode);
let gridColumns = compStyles.gridTemplateColumns;
let height = compStyles.gridAutoRows;
if (gridColumns !== "none") {
let width = gridColumns.split(" ");
this.limitSpan = width.length;
this.gridColumnGap = Number.parseInt(compStyles.gridColumnGap);
this.gridRowGap = Number.parseInt(compStyles.gridRowGap);
this.gridWidth = Number.parseFloat(width[0]);
this.gridHeight = Number.parseInt(height);
}
},
checkGridColumns: function () {
if (
typeof this.resizeWidget !== "undefined" &&
this.resizeWidget.parentNode
) {
let compStylesOfParent = window.getComputedStyle(
this.resizeWidget.parentNode
);
let spans = 0;
let gridColumns = compStylesOfParent.gridTemplateColumns.split(" ");
let temp = gridColumns[0];
gridColumns.map((v) => {
if (v == temp) spans++;
});
this.limitSpan = spans;
if (this.resizeWidget.style["grid-column-end"]) {
let curClassAttributes = this.resizeWidget
.getAttribute("class")
.split(" ");
let prevColumnState =
curClassAttributes[curClassAttributes.length - 1].split(
"_"
);
let prevSpan = Number.parseInt(prevColumnState[1]);
let curColumnState =
this.resizeWidget.style["grid-column-end"].split(" ");
let curSpan = Number.parseInt(curColumnState[1]);
let span = 0;
if (this.limitSpan >= prevSpan) span = prevSpan;
else if (prevSpan > this.limitSpan) span = this.limitSpan;
else span = Math.min(curSpan, this.limitSpan);
this.resizeWidget.style["grid-column-end"] = "span " + span;
}
}
},
initResize: function () {
DIRECTIONS.map((d) => {
if (this.Drags[d])
this.addListenerMulti(
this.Drags[d],
EVENTS[0],
this.checkCorners[d]
);
});
},
checkDragImplementation: function (e, from, to, offset, fcur, scur) {
if (e.touches) e = e.touches[0];
let other;
if (to == "top") other = "bottom";
else if (to == "bottom") other = "top";
else if (to == "left") other = "right";
else if (to == "right") other = "left";
this.removeListenerMulti(
this.Drags[from],
EVENTS[1],
this.initDrags[from]
);
this.removeListenerMulti(
this.Drags[from],
EVENTS[1],
this.initDrags[to]
);
this.removeListenerMulti(
this.Drags[from],
EVENTS[1],
this.initDrags[other]
);
this.addListenerMulti(
this.Drags[from],
EVENTS[1],
this.initDrags[from]
);
if (offset < this.cornerSize && this.Drags[to]) {
this.Drags[from].style.cursor = fcur;
this.addListenerMulti(
this.Drags[from],
EVENTS[1],
this.initDrags[to]
);
} else {
this.Drags[from].style.cursor = scur;
}
},
initDrag: function (e, idx) {
e.preventDefault();
if (!this.resizeWidget.hasAttribute("resizing")) {
this.resizeWidget.setAttribute("resizing", "");
}
let selector = document.defaultView.getComputedStyle(this.resizeWidget);
styleSheet.insertRule("iframe {pointer-events: none}", 0);
if (idx == "top" || idx == "bottom") {
this.startTop = parseInt(selector.top, 10);
this.startHeight = parseInt(selector.height, 10);
if (e.touches) this.startY = e.touches[0].clientY;
else this.startY = e.clientY;
} else {
this.startLeft = parseInt(selector.left, 10);
this.startRight = parseInt(selector.right, 10);
this.startWidth = parseInt(selector.width, 10);
if (e.touches) this.startX = e.touches[0].clientX;
else this.startX = e.clientX;
}
this.addListenerMulti(
document.documentElement,
EVENTS[0],
this.doDrags[idx]
);
this.addListenerMulti(
document.documentElement,
EVENTS[2],
this.stopDrag
);
},
initTopDrag: function (e) {
this.initDrag(e, "top");
},
initBottomDrag: function (e) {
this.initDrag(e, "bottom");
},
initLeftDrag: function (e) {
this.initDrag(e, "left");
},
initRightDrag: function (e) {
this.initDrag(e, "right");
},
//this is just for grid system
setRowEnd: function (height) {
if (this.gridHeight) {
this.heightSpan = Math.ceil(
(height - (this.heightSpan - 1) * this.gridRowGap) /
this.gridHeight
);
this.resizeWidget.style["grid-row-end"] = "span " + this.heightSpan;
this.collaborate({ rowEnd: this.heightSpan });
}
},
doTopDrag: function (e) {
let top, height;
if (e.touches) e = e.touches[0];
top = this.startTop + e.clientY - this.startY;
height = this.startHeight - e.clientY + this.startY;
if (height < 10) return;
if (!this.isGrid) {
this.resizeWidget.style.top = top + "px";
this.collaborate({ top, height });
}
this.resizeWidget.style.height = height + "px";
this.setRowEnd(height);
},
doBottomDrag: function (e) {
let height;
if (e.touches) e = e.touches[0];
height = this.startHeight + e.clientY - this.startY;
if (height < 10) return;
if (!this.isGrid) this.collaborate({ height });
this.resizeWidget.style.height = height + "px";
this.setRowEnd(height);
},
//this is just for grid system
setColumnEnd: function (width) {
if (this.gridWidth) {
let spans = Math.ceil(
(width - (this.widthSpan - 1) * this.gridColumnGap) /
this.gridWidth
);
this.widthSpan = spans > this.limitSpan ? this.limitSpan : spans;
this.resizeWidget.style["grid-column-end"] =
"span " + this.widthSpan;
this.collaborate({ columnEnd: this.widthSpan });
}
},
doLeftDrag: function (e) {
let left, right, width;
if (e.touches) e = e.touches[0];
left = this.startLeft + e.clientX - this.startX;
right = this.startRight;
width = this.startWidth - e.clientX + this.startX;
if (width < 10) return;
if (!this.isGrid) {
this.resizeWidget.style.left = "";
this.resizeWidget.style.right = right + "px";
this.collaborate({ left, width });
}
this.resizeWidget.style.width = width + "px";
this.setColumnEnd(width);
},
doRightDrag: function (e) {
let width;
if (e.touches) e = e.touches[0];
width = this.startWidth + e.clientX - this.startX;
if (width < 10) return;
if (!this.isGrid) this.collaborate({ width });
this.resizeWidget.style.width = width + "px";
this.setColumnEnd(width);
},
detectGrid: function () {
let isGrid = false;
if (this.resizeWidget.parentNode) {
let compStyles = window.getComputedStyle(
this.resizeWidget.parentNode
);
isGrid = compStyles.display === "grid";
}
this.isGrid = isGrid;
return isGrid;
},
stopDrag: function (e) {
styleSheet.deleteRule(0);
if (this.resizeWidget.hasAttribute("resizing")) {
this.resizeWidget.removeAttribute("resizing", "");
}
//this is just for grid system
if (this.detectGrid()) {
this.resizeWidget.style.width = null;
this.resizeWidget.style.height = null;
this.resizeWidget.style.left = null;
this.resizeWidget.style.top = null;
if (this.widthSpan)
this.resizeWidget.setAttribute(
"class",
this.originClassAttribute +
" grid-column-end:span_" +
this.widthSpan
);
if (this.heightSpan)
this.resizeWidget.setAttribute(
"class",
this.originClassAttribute +
" grid-row-end:span_" +
this.heightSpan
);
}
DIRECTIONS.map((d) => {
this.removeListenerMulti(
document.documentElement,
EVENTS[0],
this.doDrags[d]
);
});
this.removeListenerMulti(
document.documentElement,
EVENTS[2],
this.stopDrag
);
},
checkLeftCorners: function (e) {
let offset =
e.clientY -
this.getDistance(this.Drags["left"], true) +
document.documentElement.scrollTop;
if (offset > 10) {
offset =
this.getDistance(this.Drags["left"], true) +
this.Drags["left"].clientHeight -
e.clientY -
document.documentElement.scrollTop;
this.checkDragImplementation(
e,
"left",
"bottom",
offset,
"ne-resize",
"e-resize"
);
} else {
this.checkDragImplementation(
e,
"left",
"top",
offset,
"se-resize",
"e-resize"
);
}
},
checkTopCorners: function (e) {
let offset =
e.clientX -
this.getDistance(this.Drags["top"], false) +
document.documentElement.scrollTop;
if (offset > 10) {
offset =
this.getDistance(this.Drags["top"], false) +
this.Drags["top"].clientWidth -
e.clientX -
document.documentElement.scrollLeft;
this.checkDragImplementation(
e,
"top",
"right",
offset,
"ne-resize",
"s-resize"
);
} else {
this.checkDragImplementation(
e,
"top",
"left",
offset,
"se-resize",
"s-resize"
);
}
},
checkBottomCorners: function (e) {
let offset =
e.clientX -
this.getDistance(this.Drags["bottom"], false) +
document.documentElement.scrollTop;
if (offset > 10) {
offset =
this.getDistance(this.Drags["bottom"], false) +
this.Drags["bottom"].clientWidth -
e.clientX -
document.documentElement.scrollLeft;
this.checkDragImplementation(
e,
"bottom",
"right",
offset,
"se-resize",
"s-resize"
);
} else {
this.checkDragImplementation(
e,
"bottom",
"left",
offset,
"ne-resize",
"s-resize"
);
}
},
checkRightCorners: function (e) {
let offset =
e.clientY -
this.getDistance(this.Drags["right"], true) +
document.documentElement.scrollTop;
if (offset > 10) {
offset =
this.getDistance(this.Drags["right"], true) +
this.Drags["right"].clientHeight -
e.clientY -
document.documentElement.scrollTop;
this.checkDragImplementation(
e,
"right",
"bottom",
offset,
"se-resize",
"e-resize"
);
} else {
this.checkDragImplementation(
e,
"right",
"top",
offset,
"ne-resize",
"e-resize"
);
}
},
bindListeners: function () {
this.doDrags["left"] = this.doLeftDrag.bind(this);
this.doDrags["top"] = this.doTopDrag.bind(this);
this.doDrags["right"] = this.doRightDrag.bind(this);
this.doDrags["bottom"] = this.doBottomDrag.bind(this);
this.stopDrag = this.stopDrag.bind(this);
this.checkCorners["left"] = this.checkLeftCorners.bind(this);
this.checkCorners["top"] = this.checkTopCorners.bind(this);
this.checkCorners["bottom"] = this.checkBottomCorners.bind(this);
this.checkCorners["right"] = this.checkRightCorners.bind(this);
this.initDrags["top"] = this.initTopDrag.bind(this);
this.initDrags["left"] = this.initLeftDrag.bind(this);
this.initDrags["bottom"] = this.initBottomDrag.bind(this);
this.initDrags["right"] = this.initRightDrag.bind(this);
},
getDistance: function (elem, flag) {
var location = 0;
if (!elem) return;
if (elem.offsetParent) {
do {
location += flag ? elem.offsetTop : elem.offsetLeft;
elem = elem.offsetParent;
} while (elem);
}
return location >= 0 ? location : 0;
},
// Bind multiiple events to a listener
addListenerMulti: function (element, eventNames, listener) {
var events = eventNames.split(" ");
for (var i = 0, iLen = events.length; i < iLen; i++) {
element.addEventListener(events[i], listener, { passive: false });
}
},
// Remove multiiple events from a listener
removeListenerMulti: function (element, eventNames, listener) {
var events = eventNames.split(" ");
for (var i = 0, iLen = events.length; i < iLen; i++) {
element.removeEventListener(events[i], listener, {
passive: false
});
}
},
collaborate: function ({ left, top, width, height, rowEnd, columnEnd }) {
if (window.CoCreate && window.CoCreate.text) {
let domTextEditor = this.resizeWidget.closest("[contenteditable]");
if (domTextEditor) {
if (left)
CoCreate.text.setStyle({
domTextEditor,
target: this.resizeWidget,
property: "left",
value: left + "px"
});
if (top)
CoCreate.text.setStyle({
domTextEditor,
target: this.resizeWidget,
property: "top",
value: top + "px"
});
if (width)
CoCreate.text.setStyle({
domTextEditor,
target: this.resizeWidget,
property: "width",
value: width + "px"
});
if (height)
CoCreate.text.setStyle({
domTextEditor,
target: this.resizeWidget,
property: "height",
value: height + "px"
});
if (rowEnd)
CoCreate.text.setStyle({
domTextEditor,
target: this.resizeWidget,
property: "grid-row-end",
value: "span " + rowEnd
});
if (columnEnd)
CoCreate.text.setStyle({
domTextEditor,
target: this.resizeWidget,
property: "grid-column-end",
value: "span " + columnEnd
});
}
}
}
};
// Function to auto-resize the element based on its content
function autoResizeElement(element) {
if (!element) return;
if (element.tagName === "TEXTAREA") {
element.style.height = "auto";
let scrollHeight;
if (element.contentDocument) {
scrollHeight = element.contentDocument.body.scrollHeight;
} else {
// For other elements, get scrollHeight directly
scrollHeight = element.scrollHeight;
}
// Set the height of the element to the calculated scrollHeight
element.style.height = scrollHeight + "px";
} else if (element.tagName === "SELECT" && element.multiple) {
element.size = Math.max(element.options.length, 1); // Ensure at least 1 visible option
}
}
// Observe the resize of the element
function observeElementResize(element) {
const resizeObserver = new ResizeObserver(() => {
autoResizeElement(element);
});
resizeObserver.observe(element);
}
// Listen for input events on the entire document
function initAutoHeight(element) {
if (
element &&
!(element instanceof HTMLCollection) &&
!(element instanceof NodeList) &&
!Array.isArray(element)
) {
element = [element];
} else if (!element) {
element = document.querySelectorAll('[height="auto"]');
// Add event listener for iframes on load
document.addEventListener(
"input",
function (event) {
// Check if the target element has height="auto"
if (event.target.getAttribute("height") === "auto") {
autoResizeElement(event.target);
}
},
false
);
// if (element.contentDocument) {
// // Create a MutationObserver for changes within the iframe's body
// const observer = new MutationObserver(autoResizeElement);
// observer.observe(iframeDocument.body, {
// childList: true,
// subtree: true
// });
// // Handle iframe document changes (e.g., if src or srcdoc is modified)
// element.addEventListener("load", () => {
// observer.disconnect(); // Disconnect the previous observer
// observeIframeContent(); // Reinitialize the observer on new document
// });
// }
}
for (let i = 0; i < element.length; i++) {
if (element[i].tagName === "TEXTAREA") {
element[i].rows = 1;
}
autoResizeElement(element[i]);
observeElementResize(element[i]); // Observe resize changes.
}
}
initAutoHeight();
observer.init({
name: "CoCreateResize",
types: ["addedNodes"],
selector: "[resizable]:not([resizable='false'])",
callback: function (mutation) {
coCreateResize.initElement(mutation.target);
}
});
observer.init({
name: "CoCreateResize",
types: ["addedNodes"],
selector: "[height='auto']",
callback: function (mutation) {
initAutoHeight(mutation.target);
}
});
observer.init({
name: "CoCreateResize",
types: ["addedNodes"],
selector: "[height='auto'] option",
callback: function (mutation) {
autoResizeElement(mutation.target.parentElement);
}
});
coCreateResize.init({
selector: "[resizable]:not([resizable='false'])",
dragLeft: "[resize='left']",
dragRight: "[resize='right']",
dragTop: "[resize='top']",
dragBottom: "[resize='bottom']"
});
export default coCreateResize;