Browse Source

Move Resizer to TS

pull/4746/head
Artur Arseniev 4 years ago
parent
commit
ed96500a68
  1. 457
      src/utils/Resizer.ts

457
src/utils/Resizer.js → src/utils/Resizer.ts

@ -1,75 +1,221 @@
import { bindAll, defaults, isFunction, each } from 'underscore'; import { bindAll, defaults, isFunction, each } from 'underscore';
import { on, off, normalizeFloat } from './mixins'; import { on, off, normalizeFloat } from './mixins';
var defaultOpts = { type Position = {
// Function which returns custom X and Y coordinates of the mouse x: number;
mousePosFetcher: null, y: number;
// 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,
onUpdateContainer: () => {},
// Resize unit step
step: 1,
// Minimum dimension
minDim: 10,
// 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) => { type RectDim = {
t: number;
l: number;
w: number;
h: number;
};
type BoundingRect = {
left: number;
top: number;
width: number;
height: number;
};
type CallbackOptions = {
docs: any;
config: any;
el: HTMLElement;
resizer: Resizer;
};
interface ResizerOptions {
/**
* Function which returns custom X and Y coordinates of the mouse.
*/
mousePosFetcher?: (ev: Event) => Position;
/**
* Indicates custom target updating strategy.
*/
updateTarget?: (el: HTMLElement, rect: RectDim, opts: any) => void;
/**
* Function which gets HTMLElement as an arg and returns it relative position
*/
posFetcher?: (el: HTMLElement, opts: any) => BoundingRect;
/**
* Indicate if the resizer should keep the default ratio.
* @default false
*/
ratioDefault?: boolean;
/**
* On resize start callback.
*/
onStart?: (ev: Event, opts: CallbackOptions) => void;
/**
* On resize move callback.
*/
onMove?: (ev: Event) => void;
/**
* On resize end callback.
*/
onEnd?: (ev: Event, opts: CallbackOptions) => void;
/**
* On container update callback.
*/
onUpdateContainer?: (opts: any) => void;
/**
* Resize unit step.
* @default 1
*/
step?: number;
/**
* Minimum dimension.
* @default 10
*/
minDim?: number;
/**
* Maximum dimension.
* @default Infinity
*/
maxDim?: number;
/**
* Unit used for height resizing.
* @default 'px'
*/
unitHeight?: string;
/**
* Unit used for width resizing.
* @default 'px'
*/
unitWidth?: string;
/**
* The key used for height resizing.
* @default 'height'
*/
keyHeight?: string;
/**
* The key used for width resizing.
* @default 'width'
*/
keyWidth?: string;
/**
* If true, will override unitHeight and unitWidth, on start, with units
* from the current focused element (currently used only in SelectComponent).
* @default true
*/
currentUnit?: boolean;
/**
* With this option enabled the mousemove event won't be altered when the pointer comes over iframes.
* @default false
*/
silentFrames?: boolean;
/**
* If true the container of handlers won't be updated.
* @default false
*/
avoidContainerUpdate?: boolean;
/**
* If height is 'auto', this setting will preserve it and only update the width.
* @default false
*/
keepAutoHeight?: boolean;
/**
* If width is 'auto', this setting will preserve it and only update the height.
* @default false
*/
keepAutoWidth?: boolean;
/**
* When keepAutoHeight is true and the height has the value 'auto', this is set to true and height isn't updated.
* @default false
*/
autoHeight?: boolean;
/**
* When keepAutoWidth is true and the width has the value 'auto', this is set to true and width isn't updated.
* @default false
*/
autoWidth?: boolean;
/**
* Enable top left handler.
* @default true
*/
tl?: boolean;
/**
* Enable top center handler.
* @default true
*/
tc?: boolean;
/**
* Enable top right handler.
* @default true
*/
tr?: boolean;
/**
* Enable center left handler.
* @default true
*/
cl?: boolean;
/**
* Enable center right handler.
* @default true
*/
cr?: boolean;
/**
* Enable bottom left handler.
* @default true
*/
bl?: boolean;
/**
* Enable bottom center handler.
* @default true
*/
bc?: boolean;
/**
* Enable bottom right handler.
* @default true
*/
br?: boolean;
/**
* Class prefix.
*/
prefix?: string;
/**
* Where to append resize container (default body element).
*/
appendTo?: HTMLElement;
}
type Handlers = Record<string, HTMLElement | null>;
const createHandler = (name: string, opts: { prefix?: string } = {}) => {
var pfx = opts.prefix || ''; var pfx = opts.prefix || '';
var el = document.createElement('i'); var el = document.createElement('i');
el.className = pfx + 'resizer-h ' + pfx + 'resizer-h-' + name; el.className = pfx + 'resizer-h ' + pfx + 'resizer-h-' + name;
@ -77,7 +223,7 @@ var createHandler = (name, opts) => {
return el; return el;
}; };
var getBoundingRect = (el, win) => { const getBoundingRect = (el: HTMLElement, win?: Window): BoundingRect => {
var w = win || window; var w = win || window;
var rect = el.getBoundingClientRect(); var rect = el.getBoundingClientRect();
return { return {
@ -88,15 +234,63 @@ var getBoundingRect = (el, win) => {
}; };
}; };
class Resizer { export default class Resizer {
opts: ResizerOptions;
container?: HTMLElement;
handlers?: Handlers;
el?: HTMLElement;
clickedHandler?: HTMLElement;
selectedHandler?: HTMLElement;
handlerAttr?: string;
startDim?: RectDim;
rectDim?: RectDim;
parentDim?: RectDim;
startPos?: Position;
delta?: Position;
currentPos?: Position;
docs?: Document[];
keys?: { shift: boolean; ctrl: boolean; alt: boolean };
mousePosFetcher?: ResizerOptions['mousePosFetcher'];
updateTarget?: ResizerOptions['updateTarget'];
posFetcher?: ResizerOptions['posFetcher'];
onStart?: ResizerOptions['onStart'];
onMove?: ResizerOptions['onMove'];
onEnd?: ResizerOptions['onEnd'];
onUpdateContainer?: ResizerOptions['onUpdateContainer'];
/** /**
* Init the Resizer with options * Init the Resizer with options
* @param {Object} options * @param {Object} options
*/ */
constructor(opts = {}) { constructor(opts = {}) {
this.opts = {
ratioDefault: false,
onUpdateContainer: () => {},
step: 1,
minDim: 10,
maxDim: Infinity,
unitHeight: 'px',
unitWidth: 'px',
keyHeight: 'height',
keyWidth: 'width',
currentUnit: true,
silentFrames: false,
avoidContainerUpdate: false,
keepAutoHeight: false,
keepAutoWidth: false,
autoHeight: false,
autoWidth: false,
tl: true,
tc: true,
tr: true,
cl: true,
cr: true,
bl: true,
bc: true,
br: true,
};
this.setOptions(opts); this.setOptions(opts);
bindAll(this, 'handleKeyDown', 'handleMouseDown', 'move', 'stop'); bindAll(this, 'handleKeyDown', 'handleMouseDown', 'move', 'stop');
return this;
} }
/** /**
@ -111,8 +305,11 @@ class Resizer {
* Setup options * Setup options
* @param {Object} options * @param {Object} options
*/ */
setOptions(options = {}) { setOptions(options: Partial<ResizerOptions> = {}) {
this.opts = defaults(options, defaultOpts); this.opts = {
...this.opts,
...options,
};
this.setup(); this.setup();
} }
@ -138,9 +335,10 @@ class Resizer {
} }
// Create handlers // Create handlers
const handlers = {}; const handlers: Handlers = {};
['tl', 'tc', 'tr', 'cl', 'cr', 'bl', 'bc', 'br'].forEach( ['tl', 'tc', 'tr', 'cl', 'cr', 'bl', 'bc', 'br'].forEach(
hdl => (handlers[hdl] = opts[hdl] ? createHandler(hdl, opts) : '') // @ts-ignore
hdl => (handlers[hdl] = opts[hdl] ? createHandler(hdl, opts) : null)
); );
for (let n in handlers) { for (let n in handlers) {
@ -162,7 +360,7 @@ class Resizer {
* Toggle iframes pointer event * Toggle iframes pointer event
* @param {Boolean} silent If true, iframes will be silented * @param {Boolean} silent If true, iframes will be silented
*/ */
toggleFrames(silent) { toggleFrames(silent?: boolean) {
if (this.opts.silentFrames) { if (this.opts.silentFrames) {
const frames = document.querySelectorAll('iframe'); const frames = document.querySelectorAll('iframe');
each(frames, frame => (frame.style.pointerEvents = silent ? 'none' : '')); each(frames, frame => (frame.style.pointerEvents = silent ? 'none' : ''));
@ -174,8 +372,8 @@ class Resizer {
* @param {HTMLElement} el * @param {HTMLElement} el
* @return {Boolean} * @return {Boolean}
*/ */
isHandler(el) { isHandler(el: HTMLElement) {
var handlers = this.handlers; const { handlers } = this;
for (var n in handlers) { for (var n in handlers) {
if (handlers[n] === el) return true; if (handlers[n] === el) return true;
@ -197,14 +395,14 @@ class Resizer {
* @return {HTMLElement} * @return {HTMLElement}
*/ */
getParentEl() { getParentEl() {
return this.el.parentElement; return this.el?.parentElement;
} }
/** /**
* Returns documents * Returns documents
*/ */
getDocumentEl() { getDocumentEl() {
return [this.el.ownerDocument, document]; return [this.el!.ownerDocument, document];
} }
/** /**
@ -213,8 +411,8 @@ class Resizer {
* @param {Object} opts Custom options * @param {Object} opts Custom options
* @return {Object} * @return {Object}
*/ */
getElementPos(el, opts = {}) { getElementPos(el: HTMLElement, opts = {}) {
var posFetcher = this.posFetcher || ''; const { posFetcher } = this;
return posFetcher ? posFetcher(el, opts) : getBoundingRect(el); return posFetcher ? posFetcher(el, opts) : getBoundingRect(el);
} }
@ -222,14 +420,14 @@ class Resizer {
* Focus resizer on the element, attaches handlers to it * Focus resizer on the element, attaches handlers to it
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
focus(el) { focus(el: HTMLElement) {
// Avoid focusing on already focused element // Avoid focusing on already focused element
if (el && el === this.el) { if (el && el === this.el) {
return; return;
} }
this.el = el; this.el = el;
this.updateContainer({ forceShow: 1 }); this.updateContainer({ forceShow: true });
on(this.getDocumentEl(), 'pointerdown', this.handleMouseDown); on(this.getDocumentEl(), 'pointerdown', this.handleMouseDown);
} }
@ -237,11 +435,11 @@ class Resizer {
* Blur from element * Blur from element
*/ */
blur() { blur() {
this.container.style.display = 'none'; this.container!.style.display = 'none';
if (this.el) { if (this.el) {
off(this.getDocumentEl(), 'pointerdown', this.handleMouseDown); off(this.getDocumentEl(), 'pointerdown', this.handleMouseDown);
this.el = null; delete this.el;
} }
} }
@ -249,21 +447,23 @@ class Resizer {
* Start resizing * Start resizing
* @param {Event} e * @param {Event} e
*/ */
start(e) { start(ev: Event) {
//Right or middel click const e = ev as PointerEvent;
// @ts-ignore Right or middel click
if (e.button !== 0) return; if (e.button !== 0) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const el = this.el; const el = this.el!;
const parentEl = this.getParentEl(); const parentEl = this.getParentEl();
const resizer = this; const resizer = this;
const config = this.opts || {}; const config = this.opts || {};
const mouseFetch = this.mousePosFetcher; const mouseFetch = this.mousePosFetcher;
const attrName = 'data-' + config.prefix + 'handler'; const attrName = 'data-' + config.prefix + 'handler';
const rect = this.getElementPos(el, { target: 'el' }); const rect = this.getElementPos(el!, { target: 'el' });
const parentRect = this.getElementPos(parentEl); const parentRect = this.getElementPos(parentEl!);
this.handlerAttr = e.target.getAttribute(attrName); const target = e.target as HTMLElement;
this.clickedHandler = e.target; this.handlerAttr = target.getAttribute(attrName)!;
this.clickedHandler = target;
this.startDim = { this.startDim = {
t: rect.top, t: rect.top,
l: rect.left, l: rect.left,
@ -296,7 +496,7 @@ class Resizer {
on(docs, 'keydown', this.handleKeyDown); on(docs, 'keydown', this.handleKeyDown);
on(docs, 'pointerup', this.stop); on(docs, 'pointerup', this.stop);
isFunction(this.onStart) && this.onStart(e, { docs, config, el, resizer }); isFunction(this.onStart) && this.onStart(e, { docs, config, el, resizer });
this.toggleFrames(1); this.toggleFrames(true);
this.move(e); this.move(e);
} }
@ -304,7 +504,8 @@ class Resizer {
* While resizing * While resizing
* @param {Event} e * @param {Event} e
*/ */
move(e) { move(ev: PointerEvent | Event) {
const e = ev as PointerEvent;
const onMove = this.onMove; const onMove = this.onMove;
const mouseFetch = this.mousePosFetcher; const mouseFetch = this.mousePosFetcher;
const currentPos = mouseFetch const currentPos = mouseFetch
@ -315,8 +516,8 @@ class Resizer {
}; };
this.currentPos = currentPos; this.currentPos = currentPos;
this.delta = { this.delta = {
x: currentPos.x - this.startPos.x, x: currentPos.x - this.startPos!.x,
y: currentPos.y - this.startPos.y, y: currentPos.y - this.startPos!.y,
}; };
this.keys = { this.keys = {
shift: e.shiftKey, shift: e.shiftKey,
@ -325,7 +526,7 @@ class Resizer {
}; };
this.rectDim = this.calc(this); this.rectDim = this.calc(this);
this.updateRect(0); this.updateRect(false);
// Move callback // Move callback
onMove && onMove(e); onMove && onMove(e);
@ -335,14 +536,14 @@ class Resizer {
* Stop resizing * Stop resizing
* @param {Event} e * @param {Event} e
*/ */
stop(e) { stop(e: Event) {
const { el } = this; const el = this.el!;
const config = this.opts; const config = this.opts;
const docs = this.docs || this.getDocumentEl(); const docs = this.docs || this.getDocumentEl();
off(docs, 'pointermove', this.move); off(docs, 'pointermove', this.move);
off(docs, 'keydown', this.handleKeyDown); off(docs, 'keydown', this.handleKeyDown);
off(docs, 'pointerup', this.stop); off(docs, 'pointerup', this.stop);
this.updateRect(1); this.updateRect(true);
this.toggleFrames(); this.toggleFrames();
isFunction(this.onEnd) && this.onEnd(e, { docs, config, el, resizer: this }); isFunction(this.onEnd) && this.onEnd(e, { docs, config, el, resizer: this });
delete this.docs; delete this.docs;
@ -351,11 +552,11 @@ class Resizer {
/** /**
* Update rect * Update rect
*/ */
updateRect(store) { updateRect(store: boolean) {
const el = this.el; const el = this.el!;
const resizer = this; const resizer = this;
const config = this.opts; const config = this.opts;
const rect = this.rectDim; const rect = this.rectDim!;
const updateTarget = this.updateTarget; const updateTarget = this.updateTarget;
const selectedHandler = this.getSelectedHandler(); const selectedHandler = this.getSelectedHandler();
const { unitHeight, unitWidth, keyWidth, keyHeight } = config; const { unitHeight, unitWidth, keyWidth, keyHeight } = config;
@ -369,17 +570,17 @@ class Resizer {
config, config,
}); });
} else { } else {
const elStyle = el.style; const elStyle = el.style as Record<string, any>;
elStyle[keyWidth] = rect.w + unitWidth; elStyle[keyWidth!] = rect.w + unitWidth!;
elStyle[keyHeight] = rect.h + unitHeight; elStyle[keyHeight!] = rect.h + unitHeight!;
} }
this.updateContainer(); this.updateContainer();
} }
updateContainer(opt = {}) { updateContainer(opt: { forceShow?: boolean } = {}) {
const { opts, container, el } = this; const { opts, container, el } = this;
const { style } = container; const { style } = container!;
if (!opts.avoidContainerUpdate && el) { if (!opts.avoidContainerUpdate && el) {
// On component resize container fits the tool, // On component resize container fits the tool,
@ -390,8 +591,8 @@ class Resizer {
if (opt.forceShow) style.display = 'block'; if (opt.forceShow) style.display = 'block';
} }
this.onUpdateContainer({ this.onUpdateContainer?.({
el: container, el: container!,
resizer: this, resizer: this,
opts: { opts: {
...opts, ...opts,
@ -420,7 +621,8 @@ class Resizer {
* Handle ESC key * Handle ESC key
* @param {Event} e * @param {Event} e
*/ */
handleKeyDown(e) { handleKeyDown(e: Event) {
// @ts-ignore
if (e.keyCode === 27) { if (e.keyCode === 27) {
// Rollback to initial dimensions // Rollback to initial dimensions
this.rectDim = this.startDim; this.rectDim = this.startDim;
@ -432,13 +634,14 @@ class Resizer {
* Handle mousedown to check if it's possible to start resizing * Handle mousedown to check if it's possible to start resizing
* @param {Event} e * @param {Event} e
*/ */
handleMouseDown(e) { handleMouseDown(e: Event) {
var el = e.target; const el = e.target as HTMLElement;
if (this.isHandler(el)) { if (this.isHandler(el)) {
this.selectedHandler = el; this.selectedHandler = el;
this.start(e); this.start(e);
} else if (el !== this.el) { } else if (el !== this.el) {
this.selectedHandler = ''; delete this.selectedHandler;
this.blur(); this.blur();
} }
} }
@ -447,17 +650,17 @@ class Resizer {
* All positioning logic * All positioning logic
* @return {Object} * @return {Object}
*/ */
calc(data) { calc(data: Resizer) {
let value; let value;
const opts = this.opts || {}; const opts = this.opts || {};
const step = opts.step; const step = opts.step!;
const startDim = this.startDim; const startDim = this.startDim!;
const minDim = opts.minDim; const minDim = opts.minDim!;
const maxDim = opts.maxDim; const maxDim = opts.maxDim;
const deltaX = data.delta.x; const deltaX = data.delta!.x;
const deltaY = data.delta.y; const deltaY = data.delta!.y;
const parentW = this.parentDim.w; const parentW = this.parentDim!.w;
const parentH = this.parentDim.h; const parentH = this.parentDim!.h;
const unitWidth = this.opts.unitWidth; const unitWidth = this.opts.unitWidth;
const unitHeight = this.opts.unitHeight; const unitHeight = this.opts.unitHeight;
const startW = unitWidth === '%' ? (startDim.w / 100) * parentW : startDim.w; const startW = unitWidth === '%' ? (startDim.w / 100) * parentW : startDim.w;
@ -471,7 +674,7 @@ class Resizer {
if (!data) return; if (!data) return;
var attr = data.handlerAttr; var attr = data.handlerAttr!;
if (~attr.indexOf('r')) { if (~attr.indexOf('r')) {
value = value =
unitWidth === '%' unitWidth === '%'
@ -510,7 +713,7 @@ class Resizer {
} }
// Enforce aspect ratio (unless shift key is being held) // Enforce aspect ratio (unless shift key is being held)
var ratioActive = opts.ratioDefault ? !data.keys.shift : data.keys.shift; var ratioActive = opts.ratioDefault ? !data.keys!.shift : data.keys!.shift;
if (attr.indexOf('c') < 0 && ratioActive) { if (attr.indexOf('c') < 0 && ratioActive) {
var ratio = startDim.w / startDim.h; var ratio = startDim.w / startDim.h;
if (box.w / box.h > ratio) { if (box.w / box.h > ratio) {
@ -530,9 +733,3 @@ class Resizer {
return box; return box;
} }
} }
export default {
init(opts) {
return new Resizer(opts);
},
};
Loading…
Cancel
Save