AI Даём возможность пользователям сайта самим менять размер блоков, потянув указателем мыши за край или угол

AI

Редактор
Регистрация
23 Август 2023
Сообщения
3 641
Лучшие ответы
0
Реакции
0
Баллы
243
Offline
#1

Иллюстрация

В статье представлено всё необходимое, чтобы осуществить вынесенное в заголовок (плюс поддержка сенсорного ввода), а так же готовое open source решение, которое можно просто подключить и пользоваться.

Неожиданное решение "из коробки"


CSS resize на MDN

При написании статьи неожиданно наткнулся на CSS-свойство resize, которое делает почти то, что нам нужно. Хотя не обошлось без нескольких "ложек дёгтя".
Страница на MDN https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/resize

Для использования необходимо указать CSS-свойство "resize" у целевого блока, вписав:


  • horizontal - для включения возможности изменения размеров по горизонтали;


  • vertical - для изменения по вертикали;


  • both - оба случая.

Для работы нужно тянуть только за значок треугольника, расположенный в правом нижнем углу блока.

Минусы:


  • на момент написания статьи, поддержка браузерами ограничивается только самыми свежими десктопными версиями Firefox и Chrome (и его производными);


  • управление осуществляется не с помощью границ сторон и углов, а только путём взаимодействия со значком в правом нижнем углу, что делает не интуитивным и, вполне возможно, крайне неудобным изменения размеров блоков, расположенных, к примеру, с права или внизу страницы;


  • не понятно, как настраивается внешний вид того значка треугольника, печаль для дизайнеров (если будет так же, как с полосами прокрутки, то дело обстоит - "очень не очень");


  • дополнительные ограничения на поддерживаемые CSS-свойства для блоков.

Впрочем, для ряда случаев такой подход может оказаться вполне приемлемым.

Решение с помощью JS-скрипта


Посмотреть "в живую" можно тут https://admtoha.is-a.dev/html/demo_resizable_blocks.html (синим цветом выделены активные стороны/углы).

Нам понадобятся следующие функции, синглтон-объекты


  • get_node_style(node, style_prop[, pseudo_el]) - возвращает значения CSS-свойства с именем style_prop DOM-ноды node


  • scroll_locker - синглтон, который блокирует скроллинг страницы, что необходимо для корректной работы скрипта на мобильных устройствах, где движение по сенсорному экрану по умолчанию считывается, как сигнал к прокрутке


  • move_fix(mousemove_listener[, cursor_style][, extra_mouseup_listener]) - функция, назначение которой, убирать все мешающие негативные эффекты от движения по странице с зажатой левой клавишей мыши и аналогичного действия при сенсорном управлении (отключается выделение, прокрутка), а так же установить стиль курсора и передавать корректные данные в обработчик движения по странице mousemove_listener


  • make_resizable(node[, options]) - собственно та самая нужная нам функция, которая для ноды node создаёт активные для взаимодействия с пользователем зоны на краях сторон и углов согласно опциям options, то есть делает то, что вынесено в заголовок статьи

Рассмотрим код.

/*
Возвращает значение CSS-свойства style_prop для DOM-ноды node.
Может брать в качестве style_prop массив имён CSS-свойств,
в этом случае функцией возвращаться будет объект с ключами-именами свойств и значениям - соотв. значениями этих свойсв.
-------
Аргументы:
node (Object HTMLElement) - нода для анализа
style_prop (String or Array) - имя CSS-свойства либо их список
[ pseudo_el ] (string) - имя CSS псевдо-элемента
-------
Возвращает:
String or Object
-------
Examples:
get_node_style(node, 'transition-duration') // '0.3s'
get_node_style(node, 'height', '::before') // '10px'
get_node_style(node, ['transition-property', 'transition-duration']) // {'transition-property': 'all', 'transition-duration': '0.3s'}
*/
const get_node_style = (node, style_prop, pseudo_el) => {
if(typeof(style_prop) === 'string') return getComputedStyle(node, pseudo_el).getPropertyValue(style_prop);
else if(Array.isArray(style_prop)) return style_prop.reduce((acc, curr) => ({...acc, [curr] : getComputedStyle(node, pseudo_el).getPropertyValue(curr)}), {});
};


/*
Объект scroll_locker
Блокирует прокрутку window
-------
Методы:
.lock() - блокировка
Return:
undefined
-------
.unlock() - разблокировка
Return:
undefined
*/
const scroll_locker = {
old_value: null,
lock(){
this.old_value = get_node_style(document.body, 'overflow') || 'visible';
document.body.style.overflow = 'hidden';
},
unlock(){
document.body.style.overflow = this.old_value;
}
}


/*
Функция move_fix
Убирает все мешающие негативные эффекты от движения по странице с зажатей левой клавишей мыши
и аналогичного действия при сенсорном управлении (отключается выделение, прокрутка),
а так же устанавливает стиль курсора и передаёт корректные данные в обработчик движения по странице mousemove_listener;
-------
Аргументы:
mousemove_listener (Function) - обработчик движения мыши (+ аналог сенсорного управления)
[ cursor_style ] (String) - CSS стиль курсора, который устанавливается в момент запуска функции
[ extra_mouseup_listener ] (Function) - обработчик "отжатия" левой кнопки мыши (+ аналог сенсорного управления)
Return:
undefined
*/
const move_fix = (mousemove_listener, cursor_style = null, extra_mouseup_listener = null) => {
let iframe_fix = null, touchmove_listener, mouseup_listener, current_event;

/* ищем iframe на странице; если найдены, покрываем всё пространство прозрачным div'ом;
если этого не сделать, то движения над ифреймами будут работать некорректно */
if(document.querySelector('iframe')){
iframe_fix = document.createElement('div');
iframe_fix.style = 'position: absolute; top: 0; left: 0; height: ' + document.body.offsetHeight + 'px; width: ' + document.body.offsetWidth + 'px;';
document.body.append(iframe_fix);
};
const move_listener = event => {
current_event = event;
window.requestAnimationFrame(() => mousemove_listener(current_event))
};

/* устанавливаем обработчики движения для document.body;
для сенсорного управления корректируем входящие данные */
document.body.addEventListener('mousemove', move_listener, {passive: true});
document.body.addEventListener('touchmove', touchmove_listener = event => {
if(event.changedTouches) event = event.changedTouches[0];
move_listener(event);
}, {passive: true});

/* орбабатываем прекращение движения, другими словами: завершение работы данной функции;
удаляем все обработчики, блокировки, чистим за собой */
document.body.addEventListener('mouseup', mouseup_listener = event => {
if(event.changedTouches) event = event.changedTouches[0];

/* удаляем все обработчики */
document.body.removeEventListener('mousemove', move_listener);
document.body.removeEventListener('touchmove', touchmove_listener);
document.body.removeEventListener('touchend', mouseup_listener);
document.body.removeEventListener('touchcancel', mouseup_listener);
document.body.removeEventListener('mouseup', mouseup_listener);

/* удаляем прозрачный div, который боролся с ифреймами, если тот есть */
if(iframe_fix){
iframe_fix.remove();
iframe_fix = null;
}

/* разблокируем прокрутку страницы */
scroll_locker.unlock();

/* разблокируем выделение */
document.body.onselectstart = () => true;

/* запускаем обработчик mouseup, если требуется */
if(extra_mouseup_listener) extra_mouseup_listener(event);
document.body.style.cursor = 'auto';
}, {passive: true});
document.body.addEventListener('touchend', mouseup_listener, {passive: true});
document.body.addEventListener('touchcancel', mouseup_listener, {passive: true});

/* блокируем прокрутку */
scroll_locker.lock();

/* блокируем выделение на странице */
document.body.onselectstart = () => false;

/* устанавливаем курсор, если требуется */
if(cursor_style) document.body.style.cursor = cursor_style;
};



/*
Делает для DOM-ноды node возможность изменения размера путём взаимодействия курсора
или сенсорного аналога с активными зонами, создаваемыми согласно опциям options.
Схема активных зон:

left-top _ top _ right-top

| |

left right

| |

left-bottom _ bottom _ right-bottom

Список активных зон:
top - верхняя сторона
bottom - нижняя сторона
left - левая сторона
right - правая сторона
left-top - левый верхний угол
left-bottom - левый нижний угол
right-top - правый верхний угол
right-bottom - правый нижний угол
-------
Аргументы:
node - (HTMLElement) целевая нода
[ options ] - (Object) опции
{
zone_ls: (Array) - массив-список имён активных зон (по умолчанию: ['right', 'right-bottom', 'bottom'])
active_size: (Number Integer) - размер активных зон в пикселях (по умолчанию: 25)
}
-------
Возвращает:
undefined
-------
Примеры:
make_resizable(document.getElementById('resizable_div'), {zone_ls: ['right', 'right-bottom']})
make_resizable(document.getElementById('resizable_div'), {active_size: 30})
*/
const make_resizable = (node, options = {}) => {
if(!options.zone_ls) options.zone_ls = ['right', 'right-bottom', 'bottom']; // устанавливаем активные зоны по умолчанию
if(!options.active_size) options.active_size = 25; // устанавливаем размер активных зон по умолчанию
const node_style = get_node_style(node, ['min-height', 'min-width', 'display', 'border-left-width', 'border-right-width', 'border-top-width', 'border-bottom-width', 'position']);

/* устанавливаем минимальные размеры */
options.min_height = parseInt(node_style['min-height']) || 100;
options.min_width = parseInt(node_style['min-width']) || 100;
const
left_border_size = parseInt(node_style['border-left-width']) || 0,
right_border_size = parseInt(node_style['border-right-width']) || 0,
top_border_size = parseInt(node_style['border-top-width']) || 0,
bottom_border_size = parseInt(node_style['border-bottom-width']) || 0,
mode_position_absolute = ['fixed', 'absolute'].includes(node_style['position']) || 0; // выясняем, абсолютное ли позиционирование у целевой ноды
if(mode_position_absolute) node.style.setProperty('margin', '0'); // отключаем отступы для ноды с абсолютным позиционированием (без этого будут баги)
node.style.setProperty('box-sizing', 'border-box'); // необходимо для корректного вычисления размеров для нод с рамкой
if(node.parentNode && get_node_style(node.parentNode, 'display') === 'flex') node.style.setProperty('flex-shrink', 0); // для случая с размещения ноды во flex-контейнере
let
mode = null,
lock_mode = false,
move_listener;

/* определяем активную зону и устанавливаем соответствующий стиль курсора */
node.addEventListener('mousemove', move_listener = event => {
if((event.target !== node && node.style.cursor === 'auto') || lock_mode) return;
let
x = event.offsetX,
y = event.offsetY,
w = node.offsetWidth,
h = node.offsetHeight;
mode =
options.zone_ls.includes('left-top') && y + top_border_size <= options.active_size && x + left_border_size <= options.active_size ? 'left-top'
: options.zone_ls.includes('right-bottom') && (h - y - bottom_border_size) <= options.active_size && (w - x - right_border_size) <= options.active_size ? 'right-bottom'
: options.zone_ls.includes('right-top') && y + top_border_size <= options.active_size && (w - x - right_border_size) <= options.active_size ? 'right-top'
: options.zone_ls.includes('left-bottom') && (h - y - bottom_border_size) <= options.active_size && x + left_border_size <= options.active_size ? 'left-bottom'
: options.zone_ls.includes('top') && y + top_border_size <= options.active_size ? 'top'
: options.zone_ls.includes('bottom') && (h - y - bottom_border_size) <= options.active_size ? 'bottom'
: options.zone_ls.includes('left') && x + left_border_size <= options.active_size ? 'left'
: options.zone_ls.includes('right') && (w - x - right_border_size) <= options.active_size ? 'right'
: null
;
const cursor_style =
['left-top', 'right-bottom'].includes(mode) ? 'nw-resize'
: ['right-top', 'left-bottom'].includes(mode) ? 'ne-resize'
: ['top', 'bottom'].includes(mode) ? 'n-resize'
: ['left', 'right'].includes(mode) ? 'w-resize'
: 'auto'
;
if(node.style.cursor != cursor_style) node.style.cursor = cursor_style;
}, {passive: true});
node.addEventListener('touchmove', move_listener, {passive: true});
let mousedown_listener;

/* активация активной зоны (mousedown или touchstart) */
node.addEventListener('mousedown', mousedown_listener = event => {
if(event.target !== node) return; // исправление для Firefox; чесно говоря уже не помню, в каких обстоятельствах оно необходимо и актуально ли на сегодня вообще
if(node.offsetTop + node.offsetHeight >= document.body.offsetHeight - 20) document.body.style.height = (node.offsetTop + node.offsetHeight + 20) + 'px';
if(event.changedTouches){
event = event.changedTouches[0];
event.offsetX = event.pageX - node.getBoundingClientRect().left - window.scrollX - left_border_size + 1;
event.offsetY = event.pageY - node.getBoundingClientRect().top - window.scrollY - top_border_size + 1;
move_listener(event);
}
if(mode !== null) lock_mode = true;

/* устанавливаем начальные позиции и размеры целевой ноды и курсора */
let
x = event.offsetX,
y = event.offsetY,
w = node.offsetWidth,
h = node.offsetHeight,
t = node.offsetTop,
l = node.offsetLeft,
dx = event.pageX,
dy = event.pageY,
mousemove_listener,
mouseup_listener;

/* обрабатываемы движение */
if(mode !== null) move_fix(event => {
if(event.changedTouches) event = event.changedTouches[0];
let
xx = event.pageX - dx, // сдвиг по горизонтали
yy = event.pageY - dy; // сдвиг по вертикали
switch(mode){
case 'left-top': // левый верхний угол
if((xx < 0) || ((xx > 0) && (w - xx >= options.min_width))){
if(mode_position_absolute) node.style.left = l + xx + 'px'; // в случае с абсолютным позиционированием изменяем значение свойства left ноды
node.style.width = w - xx + 'px'; // изменяем ширину целевой ноды
}
if((yy < 0) || ((yy > 0) && (h - yy >= options.min_height))){
if(mode_position_absolute) node.style.top = t + yy + 'px'; // в случае с абсолютным позиционированием изменяем значение свойства top ноды
node.style.height = h - yy + 'px'; // изменяем высоту целевой ноды
}
break;
case 'top': // верхняя сторона
if((yy < 0) || ((yy > 0) && (h - yy >= options.min_height))){
if(mode_position_absolute) node.style.top = t + yy + 'px';
node.style.height = h - yy + 'px';
}
break;
case 'right-top': // правый верхний угол
if(((xx > 0) || (xx < 0)) && (w + xx) >= options.min_width) node.style.width = w + xx + 'px';
if((yy < 0) || ((yy > 0) && (h - yy >= options.min_height))){
if(mode_position_absolute) node.style.top = t + yy + 'px';
node.style.height = h - yy + 'px';
}
break;
case 'right': // правая сторона
if(((xx > 0) || (xx < 0)) && (w + xx) >= options.min_width) node.style.width = w + xx + 'px';
break;
case 'right-bottom': // провый нижний угол
if(event.pageY >= document.body.offsetHeight - 20) document.body.style.height = event.pageY + 20 + 'px';
if(((xx > 0) || (xx < 0)) && (w + xx) >= options.min_width) node.style.width = w + xx + 'px';
if(((yy > 0) || (yy < 0)) && (h + yy) >= options.min_height) node.style.height = h + yy + 'px';
break;
case 'bottom': // нижняя сторона
if(event.pageY >= document.body.offsetHeight - 20) document.body.style.height = event.pageY + 20 + 'px';
if(((yy > 0) || (yy < 0)) && (h + yy) >= options.min_height) node.style.height = h + yy + 'px';
break;
case 'left-bottom': // левый нижний угол
if(event.pageY >= document.body.offsetHeight - 20) document.body.style.height = event.pageY + 20 + 'px';
if((xx < 0) || ((xx > 0) && (w - xx >= options.min_width))){
if(mode_position_absolute) node.style.left = l + xx + 'px';
node.style.width = w - xx + 'px';
}
if(((yy > 0) || (yy < 0)) && (h + yy) >= options.min_height) node.style.height = h + yy + 'px';
break;
case 'left': // левая сторона
if((xx < 0) || ((xx > 0) && (w - xx >= options.min_width))){
if(mode_position_absolute) node.style.left = l + xx + 'px';
node.style.width = w - xx + 'px';
}
break;
}
}, node.style.cursor, () => lock_mode = false);
}, {passive: true});
node.addEventListener('touchstart', mousedown_listener, {passive: true});
};

Замечание. Наличие полос прокрутки у целевой ноды влияет на доступность активных зон, так как оные эти зоны перекрывают. Поэтому учитывайте это, делая поправку на размер активных зон и размер рамки. Без ущерба для дизайна можно сделать достаточно широкую прозрачную рамку (border-right) со стороны с активной зоной и полосой прокрутки.

Готовое решение


Называется resizable_blocks.

Лежит на Гитхабе здесь https://github.com/admtoha/resizable_blocks

Интерактивная демонстрация https://admtoha.is-a.dev/html/demo_resizable_blocks.html


resizable_blocks live demo

Как пользоваться


  • Подключите файл resizable_blocks.js к вашей странице

<script language="JavaScript" src="./resizable_blocks.js"></script>

  • Укажите у целевых блоков атрибут "data-resizable-blocks" и заполните опциями его значение согласно вашим предпочтениям, указав через запятую активные (доступные для взаимодействия с пользователем) стороны и углы.
    Пример:

<!-- Устанавливаются активными правая сторона, нижняя сторона и правый нижний угол -->
<div data-resizable-blocks='right, bottom, right-bottom'> ... </div>

Все доступные опции:


  1. top - верхняя сторона блока


  2. right - правая сторона


  3. bottom - нижняя сторона


  4. left - левая сторона


  5. left-top - левый верхний угол


  6. left-bottom - левый нижний угол


  7. right-top - правый верхний угол


  8. right-bottom - правый нижний угол


  9. active_size: size* - размер "активной зоны" в пикселях (по умолчанию - 25); имеется ввиду размер того доступного пространства с края целевого блока, которое является интерактивным

По умолчанию, если активные стороны и углы не заданы, устанавливается значение: right, bottom, right-bottom.

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

При необходимости вы можете обратиться к функции обработки блоков напрямую:

make_resizable(node, options)
Arguments:
node - (HTMLElement) the target node
[ options ] - (Object) options
{
zone_ls: (Array) - list of active zone numbers (default: ['right', 'right-bottom', 'bottom'])
active_size: (Number Integer) - size/width of active zones in pixels (default: 25)
}
-------
Return:
undefined

P.S. Для написания кода и статьи ИИ не использовался.
P.P.S. Телеграмм канала у меня нет, поэтому подписываться некуда, извините.
 
Яндекс.Метрика Рейтинг@Mail.ru
Сверху Снизу