Выдался ещё один свободный вечер, сегодня будем делать полноценный 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 — максимальный отступ от границ в пикселях (по умолчанию 4px), при котором можно начинать ресайз. Т.е. при наведении мышью ближе чем на 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 );
});
Вот впрочем и всё, что я хотел сказать.
Автор: DiphenylOxalate