Javascript-форум (https://javascript.ru/forum/)
-   Элементы интерфейса (https://javascript.ru/forum/dom-window/)
-   -   Как правильно сделать drag и scroll? (https://javascript.ru/forum/dom-window/77037-kak-pravilno-sdelat-drag-i-scroll.html)

gsdev99 16.03.2019 18:59

Как правильно сделать drag и scroll?
 
Все привет. Ребята, подскажите пожалуйста, как правильно сделать сделать следующий эффект:
https://chel.kassy.ru/event/2-8669/hall/
Слева есть div со span внутри. Перетаскивая его, мы скролим вверх и вниз некий выбранный контейнер.
Как подобное реализовать. Может есть какие-то библиотеки. Либо же целесообразнее написать данный эффект самому?
Важно, без использования jquery.

Malleys 17.03.2019 04:07

Для решения этой задачи можно создать свой собственный элемент <element-map>, который показывает для некоторого элемента окошко, в котором указывается, в каком месте этот элемент виден, т. н. «навигатор».

Допустим, у нас есть элемент #app, внутри которого находится значительно бо́льшая по размеру картинка.
<section id="app">
	<img>
</section>


Элемент <element-map> может располагаться внутри #app, в таком случае <element-map> использует #app, как просматриваемую область.
<section id="app">
	<element-map></element-map>
	<img>
</section>


Но вы можете захотеть, чтобы «навигатор» насполагался вне просматриваемой области. В таком случае можно добавить атрибут for, который в качестве значения принимает id просматриваемой области.
<section id="app">
	<img>
</section>
<element-map for="app"></element-map>


Также вы можете захотеть менять размеры «навигатора», но напрямую это делать нельзя, поскольку пропорции «навигатора» зависят от просматриваемой области. Но можно указать максимальный размер «навигатора» при помощи атрибута max-size. (Например max-size="200×200", т. е. «навигатор» будет принимать размеры, ограниченные площадью 200 на 200 пикселей)

Вот реализация вышеописанного...
<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width">
	</head>
	<body>
		<script>

class ElementMap extends HTMLElement {
	constructor() {
		super();
		this.attachShadow({ mode: "open" });
		this.shadowRoot.innerHTML = `
			<style>

				:host {
					display: block;
					position: absolute;
					margin: 8px;
					background-color: rgba(255, 255, 255, .5);
					background-size: cover;
					box-shadow: inset 0 0 1px black;
					border-radius: 5px;
					backdrop-filter: blur(5px);
					overflow: hidden;
				}

				:host[for] {
					position: relative;
				}

				.handle {
					border-radius: 5px;
					position: absolute;
					cursor: grab;
					box-shadow: inset 0 0 0 1px red, 0 0 0 100vmax rgba(0, 0, 0, 0.4);
				}

			</style>
			<div class="handle"></div>
		`;

		this.applyScrollPosition = this.applyScrollPosition.bind(this);
		this.pointerHandler = this.pointerHandler.bind(this);
		
		this.handle = this.shadowRoot.querySelector(".handle");
		this.handle.addEventListener("pointerdown", this.pointerHandler);
	}

	attributeChangedCallback(name, oldValue, newValue) {
		if(name === "for" && newValue && this.ownerDocument) {
			this.forElement = this.ownerDocument.getElementById(newValue);
		}
	}

	static get observedAttributes() {
		return ["max-size", "for"];
	}

	set forElement(element) {
		this._container = element;
	}

	get forElement() {
		if("_container" in this)
			return this._container;

		let element = null;

		if(this.hasAttribute("for")) {
			const id = this.getAttribute("for");

			if(this.ownerDocument)
				element = this.ownerDocument.getElementById(id);
		} else
			element = this.parentNode;

		return this._container = element;
	}

	get maxSize() {
		if(this.hasAttribute("max-size")) {
			return this.getAttribute("max-size").trim().split(/\D/).map(Number);
		} else {
			return [150, 150];
		}
	}

	connectedCallback() {
		this.forElement.style.overflow = "auto";
		this.applyScrollPosition();
		this.forElement.addEventListener("scroll", this.applyScrollPosition);
		this.ownerDocument.defaultView.addEventListener("load", this.applyScrollPosition);
	}

	disconnectedCallback() {
		this.forElement.removeEventListener("scroll", this.applyScrollPosition);
		this.forElement.ownerDocument.defaultView.removeEventListener("load", this.applyScrollPosition);
	}

	pointerHandler(event) {
		const { screenX, screenY } = event;

		if(event.type === "pointerdown") {
			document.addEventListener("pointermove", this.pointerHandler);
			document.addEventListener("pointerup", this.pointerHandler);

			this._startX = screenX;
			this._startY = screenY;

			this._scrollLeft = this.forElement.scrollLeft;
			this._scrollTop = this.forElement.scrollTop;
		}

		if(event.type === "pointerup") {
			document.removeEventListener("pointermove", this.pointerHandler);
			document.removeEventListener("pointerup", this.pointerHandler);
		}

		if(event.type === "pointermove") {
			const dx = (screenX - this._startX) / this.ρ;
			const dy = (screenY - this._startY) / this.ρ;

			this.forElement.scrollTo(
				this._scrollLeft + dx,
				this._scrollTop + dy
			);
		}

		event.preventDefault();
	}

	applyScrollPosition() {
		const {
			offsetWidth,
			offsetHeight,
			scrollWidth,
			scrollHeight,
			scrollLeft,
			scrollTop
		} = this.forElement;

		this.ρ = Math.min(
			this.maxSize[0] / scrollWidth,
			this.maxSize[1] / scrollHeight
		);

		this.style.width  = `${this.ρ * scrollWidth }px`;
		this.style.height = `${this.ρ * scrollHeight}px`;

		this.handle.style.cssText = `
			width:  ${this.ρ * offsetWidth }px;
			height: ${this.ρ * offsetHeight}px;
			left:   ${this.ρ * scrollLeft  }px;
			top:    ${this.ρ * scrollTop   }px;
		`;
	}
}

customElements.define("element-map", ElementMap);

		</script>
		<style>::-webkit-scrollbar{height:0;width:0}</style>

		<!-- Пример -->
		<section id="app" style="max-width: 600px; height: 400px; box-shadow: 0 0 1em black;">
			<element-map style="background-image: url(http://lowbird.com/data/images/2018/03/structureoffreemasonryac3.jpg);"></element-map>
			<img src="http://lowbird.com/data/images/2018/03/structureoffreemasonryac3.jpg" alt="The Structure of Freemasonry" style="display: block;">
		</section>
		<element-map for="app" max-size="200×200"></element-map>
	</body>
</html>

рони 17.03.2019 08:23

Malleys,
:thanks:

рони 17.03.2019 08:30

Malleys,
можно пояснить, что делают строки
013
this.attachShadow({ mode: "open" });

и
057
static get observedAttributes() { 
        return ["max-size", "for"]; 
    }

?

gsdev99 17.03.2019 16:21

Огромное спасибо!

Malleys 17.03.2019 19:04

Цитата:

Сообщение от рони
можно пояснить, что делают строки

Цитата:

Сообщение от рони
this.attachShadow({ mode: "open" });

Подключает к элементу теневой DOM, который изолирован от остального DOM.

Например, если вы написали компонент, который хотите использовать на страницах, причем так, чтобы стили страниц никак не влияли на визуальное оформление компонента, то вы можете подключить теневой DOM.

Без такой изоляции в DOM применяемые компоненты могут воздействовать друг на друга (например, применённые стили из других компонентов), вот теневой DOM позволяет использовать без оглядки на используемые на странице и во всех её фрагментах стили.

Откройте настройки, например, Chrome DevTools (когда смотрите консоль, нажмите F1) и включите «Show user agent shadow DOM», теперь вы можете посмотреть как устроены, например, элементы <input type="range">, <select>, <video> и т. д. «изнутри». Так же нужно помнить, что наружу из компонента видны только те события, которые вы пробросите и никто не сможет подписаться на какой-нибудь "change" у <input> внутри компонента.

Цитата:

Сообщение от рони
static get observedAttributes() {
    return ["max-size", "for"];
}

Изменение каких атрибутов отслеживать, когда что-то из указанного поменяется, вызовется метод attributeChangedCallback

рони 17.03.2019 19:24

Malleys,
спасибо ... ваш код для меня, отрытие чего-то нового


Часовой пояс GMT +3, время: 19:39.