Заняття на вечір: ресайзинг елементів на 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 — максимальний відступ від кордонів в пікселях (за замовчуванням 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 — максимальні і мінімальні розміри елемента, а дозволити — дозволені напрямки (наприклад, значення «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 );
});


Ось втім і все, що я хотів сказати.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.