Показать сообщение отдельно
  #2 (permalink)  
Старый 17.03.2019, 04:07
Аватар для Malleys
Профессор
Отправить личное сообщение для Malleys Посмотреть профиль Найти все сообщения от Malleys
 
Регистрация: 20.12.2009
Сообщений: 1,714

Для решения этой задачи можно создать свой собственный элемент <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>
Ответить с цитированием