handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
207 lines (200 loc) • 7.7 kB
JavaScript
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* autoResize - resizes a DOM element to the width and height of another DOM element
*
* Copyright 2014, Marcin Warpechowski
* Licensed under the MIT license
*/
/* eslint-enable jsdoc/require-description-complete-sentence */
/**
* Attaches an event listener to the given element.
*
* @param {HTMLElement} element The element to observe.
* @param {string} eventName The name of the event to listen for.
* @param {Function} handler The function to call when the event is triggered.
*/
function observe(element, eventName, handler) {
element.addEventListener(eventName, handler, false);
}
/**
* Removes an event listener from an element.
*
* @param {HTMLElement} element The element to remove the event listener from.
* @param {string} eventName The name of the event to remove.
* @param {Function} handler The function to remove as a listener.
*/
function unObserve(element, eventName, handler) {
element.removeEventListener(eventName, handler, false);
}
/**
* Returns the computed style of an element.
*
* @param {Element} element The element to get the computed style from.
* @returns {CSSStyleDeclaration} The computed style of the element.
*/
function getComputedStyle(element) {
return element.ownerDocument.defaultView.getComputedStyle(element);
}
/**
* @typedef InputElementResizerConfig
* @property {number} minWidth The minimum width of the element.
* @property {number} maxWidth The maximum width of the element.
* @property {number} minHeight The minimum height of the element.
* @property {number} maxHeight The maximum height of the element.
* @property {function(HTMLElement): string} textContent The function that returns the text content to measure.
*/
/**
* @typedef InputElementResizer
* @property {function(HTMLElement, InputElementResizerConfig, boolean): void} init Initializes the resizer.
* @property {function(): void} resize Resizes the element.
* @property {function(): void} unObserve Removes the event listeners.
*/
/**
* Creates an input element resizer.
*
* @param {Document} ownerDocument The document to create the resizer for.
* @param {InputElementResizerConfig} initialOptions The configuration to extend the defaults with.
* @returns {InputElementResizer}
*/
export function createInputElementResizer(ownerDocument) {
let initialOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
const defaults = {
minHeight: 200,
maxHeight: 300,
minWidth: 100,
maxWidth: 300,
textContent: element => element.value,
...initialOptions
};
const body = ownerDocument.body;
const textHolder = ownerDocument.createTextNode('');
const textContainer = ownerDocument.createElement('span');
let observedElement;
/**
* Resizes the element.
*/
function resize() {
var _getComputedStyle, _getComputedStyle2;
textHolder.textContent = defaults.textContent(observedElement);
// Won't expand the element size for displaying body as for example, `grid`, `inline-grid` or `flex` with
// `flex-direction` set as `column`.
textContainer.style.position = 'absolute';
textContainer.style.fontSize = getComputedStyle(observedElement).fontSize;
textContainer.style.fontFamily = getComputedStyle(observedElement).fontFamily;
textContainer.style.whiteSpace = 'pre';
body.appendChild(textContainer);
const paddingStart = parseInt(((_getComputedStyle = getComputedStyle(observedElement)) === null || _getComputedStyle === void 0 ? void 0 : _getComputedStyle.paddingInlineStart) || 0, 10);
const paddingEnd = parseInt(((_getComputedStyle2 = getComputedStyle(observedElement)) === null || _getComputedStyle2 === void 0 ? void 0 : _getComputedStyle2.paddingInlineEnd) || 0, 10);
const width = textContainer.clientWidth + paddingStart + paddingEnd + 1;
body.removeChild(textContainer);
const elementStyle = observedElement.style;
elementStyle.height = `${defaults.minHeight}px`;
if (defaults.minWidth > width) {
elementStyle.width = `${defaults.minWidth}px`;
} else if (width > defaults.maxWidth) {
elementStyle.width = `${defaults.maxWidth}px`;
} else {
elementStyle.width = `${width}px`;
}
const scrollHeight = observedElement.scrollHeight ? observedElement.scrollHeight - 1 : 0;
if (defaults.minHeight > scrollHeight) {
elementStyle.height = `${defaults.minHeight}px`;
} else if (defaults.maxHeight < scrollHeight) {
elementStyle.height = `${defaults.maxHeight}px`;
elementStyle.overflowY = 'visible';
} else {
elementStyle.height = `${scrollHeight}px`;
}
}
/**
* Resizes the element after a delay.
*/
function delayedResize() {
ownerDocument.defaultView.setTimeout(resize, 0);
}
/**
* Extends the default configuration.
*
* @param {InputElementResizerConfig} config The configuration to extend the defaults with.
*/
function extendDefaults(config) {
if (config && config.minHeight) {
if (config.minHeight === 'inherit') {
defaults.minHeight = observedElement.clientHeight;
} else {
const minHeight = parseInt(config.minHeight, 10);
if (!isNaN(minHeight)) {
defaults.minHeight = minHeight;
}
}
}
if (config && config.maxHeight) {
if (config.maxHeight === 'inherit') {
defaults.maxHeight = observedElement.clientHeight;
} else {
const maxHeight = parseInt(config.maxHeight, 10);
if (!isNaN(maxHeight)) {
defaults.maxHeight = maxHeight;
}
}
}
if (config && config.minWidth) {
if (config.minWidth === 'inherit') {
defaults.minWidth = observedElement.clientWidth;
} else {
const minWidth = parseInt(config.minWidth, 10);
if (!isNaN(minWidth)) {
defaults.minWidth = minWidth;
}
}
}
if (config && config.maxWidth) {
if (config.maxWidth === 'inherit') {
defaults.maxWidth = observedElement.clientWidth;
} else {
const maxWidth = parseInt(config.maxWidth, 10);
if (!isNaN(maxWidth)) {
defaults.maxWidth = maxWidth;
}
}
}
if (!textContainer.firstChild) {
textContainer.className = 'autoResize';
textContainer.style.display = 'inline-block';
textContainer.appendChild(textHolder);
}
}
/**
* Initializes the resizer.
*
* @param {HTMLElement} elementToObserve The element to observe.
* @param {InputElementResizerConfig} config The configuration to extend the defaults with.
* @param {boolean} [doObserve=false] Whether to observe the element and resize it on every input change.
*/
function init(elementToObserve, config) {
let doObserve = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
observedElement = elementToObserve;
extendDefaults(config);
if (observedElement.nodeName === 'TEXTAREA') {
observedElement.style.resize = 'none';
observedElement.style.height = `${defaults.minHeight}px`;
observedElement.style.minWidth = `${defaults.minWidth}px`;
observedElement.style.maxWidth = `${defaults.maxWidth}px`;
observedElement.style.overflowY = 'hidden';
}
if (doObserve) {
observe(observedElement, 'input', resize);
// the keydown event is necessary for undo stack to work properly
observe(observedElement, 'keydown', delayedResize);
}
resize();
}
return {
init,
resize,
unObserve() {
unObserve(observedElement, 'input', resize);
unObserve(observedElement, 'keydown', delayedResize);
}
};
}