const DRAG_HANDLE_ICON = ` ` class DragPage extends HTMLElement { public headerRef: HTMLDivElement public titleRef: HTMLDivElement public dragHandleRef: HTMLDivElement private initWidth: number = 400 static get observedAttributes() { return ['heading', 'key', 'minimized', 'color'] } constructor() { super() } public connectedCallback() { const style = ` :root { width: auto; } :host { position: absolute; z-index: 9; background-color: #ffffff; text-align: center; border: 1px solid #000000; width: auto; } :host([active]) { z-index: 999; } :host([disabled]) #header { background-color: #9E9E9E; cursor: unset; } #header { display: flex; justify-content: space-between; padding: 10px; z-index: 10; background-color: #2196F3; color: #fff; } #drag-handle { width: 20px; height: 20px; cursor: move; } #drag-handle svg { width: 20px; height: 20px; } #heading { margin: 0 auto; } #controls { display: inline-flex; margin-left: 10px; } .btn { line-height: 9px; width: 10px; height: 10px; border: 1px solid black; border-radius: 3px; box-shadow: 1px 1px black; cursor: pointer; color: white; } .btn:not(:last-of-type) { margin: 0 5px 0 0; } .btn:hover { box-shadow: -1px -1px black; border: 1px solid black; color: white; } :host([disabled]) .btn, :host([disabled]) .btn:hover { border: 1px solid #616161; box-shadow: inset 1px 1px black; color: #424242; cursor: unset; } .btn:active { box-shadow: inset 1px 1px black; } ` const template = document.createElement('template') template.innerHTML = ` ` const shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.appendChild(template.content.cloneNode(true)) this.headerRef = this.shadowRoot!.querySelector('#header')! this.titleRef = this.shadowRoot!.querySelector('#heading')! this.dragHandleRef = this.shadowRoot!.querySelector('#drag-handle')! this.restorePosition() this.dragHandleRef.addEventListener('mousedown', this.beginElementDrag) this.dragHandleRef.addEventListener('dblclick', this.handleDblCLick) this.headerRef.querySelectorAll('.btn').forEach( (btnEl) => btnEl.addEventListener('click', this.handleBtnClick)) // set the initial width to the width of the content this.initWidth = this.getBoundingClientRect().width // update the z-index when the element is clicked this.addEventListener('click', () => { if (this.id !== 'canvas_main') { this.style.zIndex = '3' const otherPanes = Array.from(document.getElementsByTagName('drag-pane')).filter( (el: HTMLElement) => el !== this && el.id !== 'canvas_main') otherPanes.forEach( (el: HTMLElement) => el.style.zIndex = '2') } }) } public get heading(): string { return this.getAttribute('heading') || '' } public set heading(newState: string) { this.setAttribute('heading', newState) } public get minimized(): boolean { return this.getAttribute('minimized') === 'true' || this.hasAttribute('minimized') && this.getAttribute('minimized') === '' } public set minimized(newState: boolean) { if (newState) { this.setAttribute('minimized', 'true') } else { this.removeAttribute('minimized') } } public set color(newColor: string) { this.setAttribute('color', newColor) } public get color(): string { return this.getAttribute('color') || this.defaultHeaderColor } public get key(): string|undefined { return this.getAttribute('key') || undefined } public set key(newValue: string|undefined){ if (newValue) { this.setAttribute('key', newValue) } else { this.removeAttribute('key') } } attributeChangedCallback(_name: string) { if (_name === 'minimized') { if (this.minimized) { // this.style.height = '30px' this.shadowRoot!.querySelector('#content')!.style.display = 'none' this.style.width = this.initWidth + 'px' } else { // this.style.height = 'auto' this.shadowRoot!.querySelector('#content')!.style.display = 'block' this.style.width = this.initWidth + 'px' } } } private defaultHeaderColor: string = '#2196F3' private handleBtnClick = (event: MouseEvent) => { const el = event!.target as HTMLDivElement switch (el.id) { case 'close': this.dispatchEvent(new Event('remove')) this.remove() break case 'minimize': this.minimized = !this.minimized this.dispatchEvent(new Event('toggleminimize', {bubbles: true, composed: true})) break default: break } } private handleDblCLick = () => { this.minimized = !this.minimized this.dispatchEvent(new Event('toggleminimize', {bubbles: true, composed: true})) } private pos: {x: number, y: number} = {x:0, y:0} private storePosition(){ if (this.key) { window.localStorage.setItem(`drag-pane-${this.key}`, JSON.stringify(this.pos)) } } private restorePosition(){ if (this.key) { const storeValue = window.localStorage.getItem(`drag-pane-${this.key}`) if (storeValue) { this.pos = JSON.parse(storeValue) this.style.top = this.pos.y + 'px' this.style.left = this.pos.x + 'px' } } } private didMove: boolean = false // private moveThreshold: number = 5; private beginElementDrag = (event: MouseEvent) => { event.preventDefault() this.setAttribute('active', 'true') // get the mouse cursor position at startup: this.pos.x = event.clientX this.pos.y = event.clientY document.onmouseup = this.endElementDrag // call a function whenever the cursor moves: document.onmousemove = this.elementDrag } private elementDrag = (event: MouseEvent) => { event.preventDefault() // calculate the new cursor position: const pos1 = this.pos.x - event.clientX const pos2 = this.pos.y - event.clientY this.pos.x = event.clientX this.pos.y = event.clientY this.didMove = true this.dispatchEvent(new Event('dragstart', {bubbles: true, composed: true})) // set the element's new position: const newPosition = {top: this.offsetTop - pos2, left: this.offsetLeft - pos1} this.style.top = newPosition.top + 'px' this.style.left = newPosition.left + 'px' } private endElementDrag = () => { // release element document.onmouseup = null document.onmousemove = null this.setAttribute('active', 'false') if (this.didMove) { this.storePosition() this.dispatchEvent(new Event('dragend', {bubbles: true, composed: true})) this.didMove = false } } } window.customElements.define('drag-pane', DragPage) export default DragPage