Занятие на вечер: ресайзинг элементов на pure JS


Выдался ещё один свободный вечер, сегодня будем делать полноценный resizable в любую сторону для любых элементов (div, изображений, чего угодно) на чистом JavaScript.

Демо-страничка: Resizable.js

Подход №0
Сделать восемь висячих дивов-ресайзеров: четыре из них — полоски по вдоль боков элемента, ещё четыре — по углам. При ресайзе изменять размеры элемента, а также тащить дивы-ресайзеры.
Минусы очевидны:

  • много повторяющегося кода
  • неудобно: придётся работать и с элементом, и с ресайзерами
  • просто стыдно показывать кому-то такой скрипт

Подход №1
При mousemove элемента проверять, не находится ли курсор у края элемента. Если да, назначаем соответствующий стиль курсора (например, при наведении на верхний правый край будет ne-resize), если нет — назначаем курсор default.
При mousedown элемента делаем такую же проверку. Если да — начинаем ресайз в нужную сторону, если нет — ничего не делаем.

Пишем вспомогательную функцию direction (), которая и будет определять, на каком крае элемента находится курсор. Она будет возвращать либо число от 0 до 7 (0 — ресайз вверх, 1 — вправо и т.д.; 4 — ресайз вверх и вправо, 5 — вправо и вниз и т.д. по часовой стрелке). Если 8 — курсор не у края элемента, ресайз не начинать.

function direction( elem, event, pad ) {
	var res = 8;
	var pad = pad || 4;
	var pos = elem.getBoundingClientRect();
	var top = pos.top;
	var left = pos.left;
	var width = elem.clientWidth;
	var height = elem.clientHeight;
	var eTop = event.clientY;
	var eLeft = event.clientX;
	// [...]

Названия аргументов говорят за себя: elem — элемент, подлежащий ресайзингу, event — объект события mousemove или mousedown элемента, а pad — максимальный отступ от границ в пикселях (по умолчанию 4 px), при котором можно начинать ресайз. Т.е. при наведении мышью ближе чем на 4 пикселя к краю изменится курсор и при нажатии мыши начнётся ресайз.

Отталкиваясь от размеров и координат элемента, а также от координат события, выясняем у какого края находится курсор:

var isTop = eTop - top < pad;
var isRight = left + width - eLeft < pad;
var isBottom = top + height - eTop < pad;
var isLeft = eLeft - left < pad;
if ( isTop ) res = 0;
if ( isRight ) res = 1;
if ( isBottom ) res = 2;
if ( isLeft ) res = 3;

Это ещё не всё — надо также учесть ресайз по диагонали:

if ( isTop && isRight ) res = 4;
if ( isRight && isBottom ) res = 5;
if ( isBottom && isLeft ) res = 6;
if ( isLeft && isTop ) res = 7;
return res;		// если ни одно из условий не сработает,
			// то res так и останется 8

Введём вспомогательную переменную для назначения стиля курсора. Она понадобится нам в дальнейшем.

var cursors = "n w s e ne se sw nw".split(" ");

Теперь приступим непосредственно к написанию кода Resizable ().

function Resizable( elem, options ) {
	options = options || {};
	options.max = options.max || [1E17, 1E17];
	options.min = options.min || [10, 10];
	options.allow = (options.allow || "11111111").split("");
	// [...]

Здесь max и min — максимальные и минимальные размеры элемента, а allow — разрешённые направления (например, значение »11110000» запретит ресайз по диагонали).

Меняем курсор при наведении мышки близко к краю:

elem.addEventListener( "mousemove", function ( e ) {
	var dir = direction( this, e );
	if ( options.allow[dir] == "0" ) return;
	this.style.cursor = dir == 8 ? "default" : cursors[ dir ] + "-resize";
	// вот и пригодилась cursors
} );

При mousedown элемента вызываем функцию resizeStart, код которой будет дальше. Также запрещаем выделение текста при ресайзе.

elem.addEventListener( "mousedown", resizeStart );
document.body.onselectstart = function (e) { return false };

Значения options уже не будут доступны в resizeStart, поэтому закэшируем их, чтобы пользоваться в будущем.

elem.min = options.min;
elem.max = options.max;
elem.allow = options.allow;
elem.pos = elem.getBoundingClientRect();

Половина работы уже сделана — осталось только менять размеры элемента, основываясь на direction (). С ресайзом вправо и вниз всё просто — надо лишь менять высоту и ширину, а вот при ресайзе вверх и влево придётся менять и координаты элемента. Но обо всём по порядку.

function resizeStart( ev ) {
	var dir = direction( this, ev ); // вычисляем направление
	if ( this.allow[dir] == "0" ) return; // если направление не разрешено, отменяем ресайз
	document.documentElement.style.cursor = this.style.cursor = cursors[ dir ] + "-resize";
	var pos = this.getBoundingClientRect();
	var elem = this; // для работы в mousemove документа
	var height = this.clientHeight;
	var width = this.clientWidth;
	// при каждом движении мыши на документе будет
	// срабатывать resize(). Её определение будет ниже.
	document.addEventListener( "mousemove", resize );
	// при отпускании кнопки мыши удаляем обработчик
	// и ставим стандартный курсор
	document.addEventListener( "mouseup", function () {
		document.removeEventListener( "mousemove", resize );
		document.documentElement.style.cursor = elem.style.cursor = "default";
		document.body.onselectstart = null;
	});
};

Последний рубеж — внутренняя функция resize (), которая и будет осуществлять всю работу.


function resize ( e ) {
	// при ресайзе вверх либо вверх и вправо либо вверх и влево
	// изменяем высоту и отступ сверху
	// все остальные if-ы работают так же
	if ( dir == 0 || dir == 4 || dir == 7 ) {
		elem.style.top = e.clientY - ev.clientY + pos.top;
		elem.style.height = height + ev.clientY - e.clientY;
	}
	if ( dir == 1 || dir == 4 || dir == 5 ) {
		elem.style.width = e.clientX - pos.left;
	}
	if ( dir == 2 || dir == 5 || dir == 6 ) {
		elem.style.height = e.clientY - pos.top;
	}
	if ( dir == 3 || dir == 6 || dir == 7 ) {
		elem.style.left = e.clientX - ev.clientX + pos.left;
		elem.style.width = width + ev.clientX - e.clientX;
	}

Дополнительные ограничения: при ресайзе сверху вниз либо справа влево при достижении минимальной высоты либо ширины соотвественно, элемент начинает сдвигаться вниз либо вправо без изменения размеров. Поэтому делаем такую проверку:

if ( e.clientY + elem.min[1] > ev.clientY + height ) return;
if ( e.clientX + elem.min[0] > ev.clientX + width ) return;

Также проверяем, если достигнут минимальный или максимальный размер:

if ( elem.clientHeight < elem.min[1] ) elem.style.height = elem.min[1];
if ( elem.clientWidth < elem.min[0] ) elem.style.width = elem.min[0];
if ( elem.clientHeight > elem.max[1] ) elem.style.height = elem.max[1];
if ( elem.clientWidth > elem.max[0] ) elem.style.width = elem.max[0];
if ( e.clientY < pos.bottom - elem.max[1] ) elem.style.top = pos.bottom - elem.max[1];
if ( e.clientX < pos.right - elem.max[0] ) elem.style.left = pos.right - elem.max[0];

Не знаю, можно ли это было сделать элегантнее… В любом случае, надо работать и с размером элемента, и с его координатами, и с координатами события, и всё это опираясь на результат direction (), да ещё и с учётом мин./макс. размеров.

В общем, плагин готов. Исходник на js здесь, а пример работы как всегда в самом начале.

Бонус: анимированный resizable

Демо-страничка: Resizable 2.0

Принцип работы схож, только ресайзится не сам элемент, а создаваемый див:

var helper = document.createElement( "DIV" );
document.body.appendChild( helper );
helper.style.cssText = "position: fixed; border: 1px dashed black";
helper.style.width = width;
helper.style.height = height;
helper.style.top = pos.top;
helper.style.left = pos.left;

В if-ах, написанных выше, вместо elem будет helper.

При mouseup документа анимируем элемент до размеров хелпера, а хелпер удаляем:

document.addEventListener( "mouseup", function () {
	document.removeEventListener( "mousemove", resize );
	document.documentElement.style.cursor = elem.style.cursor = "default";
	document.body.onselectstart = null;
	var newpos = helper.getBoundingClientRect();
	var start = new Date().getTime();
	setTimeout( animate, 10 );
	function animate() {
		var m = (new Date().getTime() - start) / 300;
		if (m > 1) m = 1;
		elem.style.top = pos.top + (newpos.top - pos.top) * m;
		elem.style.left = pos.left + (newpos.left - pos.left) * m;
		elem.style.height = height + (helper.clientHeight - height) * m;
		elem.style.width = width + (helper.clientWidth - width) * m;
		if (m < 1) setTimeout( animate, 10 );
	}
	setTimeout( function () {document.body.removeChild( helper );}, 310 );
});

Вот впрочем и всё, что я хотел сказать.

Комментарии (2)

  • 10 февраля 2017 в 22:26

    +3

    Курсор мерцает и продолжаете учить вредному setTimeout, эх вы
  • 11 февраля 2017 в 00:04

    0

    Размер элемента некорректно резко изменяется в меньшую сторону при вытягивании его границы за пределы окна браузера.

© Habrahabr.ru