1import { Controller } from '@hotwired/stimulus'; 2import interact from 'interactjs'; 3 4const VIEWPORT_MARGIN = 12; 5const DEFAULT_WIDTH = 920; 6const DEFAULT_HEIGHT = 680; 7const MIN_WIDTH = 420; 8const MIN_HEIGHT = 320; 9 10export default class extends Controller { 11 static targets = ['window', 'handle', 'title', 'frame']; 12 13 connect() { 14 this.onDocumentClick = this.onDocumentClick.bind(this); 15 this.onResize = this.onResize.bind(this); 16 this.onKeydown = this.onKeydown.bind(this); 17 this.hasPosition = false; 18 19 document.addEventListener('click', this.onDocumentClick); 20 window.addEventListener('resize', this.onResize); 21 document.addEventListener('keydown', this.onKeydown); 22 this.setupInteractions(); 23 } 24 25 disconnect() { 26 document.removeEventListener('click', this.onDocumentClick); 27 window.removeEventListener('resize', this.onResize); 28 document.removeEventListener('keydown', this.onKeydown); 29 this.interactable?.unset(); 30 } 31 32 open(event) { 33 const href = event.params.href || event.currentTarget?.dataset.documentViewerHref || ''; 34 const title = event.params.title || event.currentTarget?.dataset.documentViewerTitle || ''; 35 this.show(href, title); 36 } 37 38 close() { 39 this.windowTarget.classList.add('d-none'); 40 this.frameTarget.src = ''; 41 this.titleTarget.textContent = ''; 42 } 43 44 center() { 45 const width = Math.min(this.windowTarget.offsetWidth || DEFAULT_WIDTH, this.maxWidth); 46 const height = Math.min(this.windowTarget.offsetHeight || DEFAULT_HEIGHT, this.maxHeight); 47 const x = Math.max(VIEWPORT_MARGIN, Math.round((window.innerWidth - width) / 2)); 48 const y = Math.max(VIEWPORT_MARGIN, Math.round((window.innerHeight - height) / 2)); 49 50 this.setGeometry({ x, y, width, height }); 51 this.hasPosition = true; 52 } 53 54 onDocumentClick(event) { 55 const trigger = event.target.closest('[data-document-viewer-href]'); 56 if (!trigger) { 57 return; 58 } 59 60 event.preventDefault(); 61 this.show(trigger.dataset.documentViewerHref || '', trigger.dataset.documentViewerTitle || ''); 62 } 63 64 onResize() { 65 if (this.windowTarget.classList.contains('d-none')) { 66 return; 67 } 68 69 this.keepInViewport(); 70 } 71 72 onKeydown(event) { 73 if (event.key !== 'Escape' || this.windowTarget.classList.contains('d-none')) { 74 return; 75 } 76 77 this.close(); 78 } 79 80 show(href, title) { 81 if (!href) { 82 return; 83 } 84 85 this.titleTarget.textContent = title || 'Document'; 86 this.frameTarget.src = href; 87 this.windowTarget.classList.remove('d-none'); 88 89 if (!this.hasPosition) { 90 this.center(); 91 } else { 92 this.keepInViewport(); 93 } 94 } 95 96 setupInteractions() { 97 this.interactable = interact(this.windowTarget) 98 .draggable({ 99 allowFrom: '[data-rental--document-viewer-target~="handle"]', 100 ignoreFrom: 'button, a, input, select, textarea', 101 listeners: { 102 start: () => this.startInteraction(), 103 move: (event) => this.move(event), 104 end: () => this.endInteraction(), 105 }, 106 }) 107 .resizable({ 108 edges: { 109 right: '.document-viewer__resize-handle', 110 bottom: '.document-viewer__resize-handle', 111 }, 112 listeners: { 113 start: () => this.startInteraction(), 114 move: (event) => this.resize(event), 115 end: () => this.endInteraction(), 116 }, 117 }); 118 } 119 120 move(event) { 121 const x = Number.parseFloat(this.windowTarget.dataset.x || '0') + event.dx; 122 const y = Number.parseFloat(this.windowTarget.dataset.y || '0') + event.dy; 123 124 this.setPosition(x, y); 125 this.hasPosition = true; 126 } 127 128 resize(event) { 129 const x = Number.parseFloat(this.windowTarget.dataset.x || '0') + event.deltaRect.left; 130 const y = Number.parseFloat(this.windowTarget.dataset.y || '0') + event.deltaRect.top; 131 132 this.setGeometry({ 133 x, 134 y, 135 width: event.rect.width, 136 height: event.rect.height, 137 }); 138 this.hasPosition = true; 139 } 140 141 keepInViewport() { 142 const rect = this.windowTarget.getBoundingClientRect(); 143 const width = Math.min(rect.width, this.maxWidth); 144 const height = Math.min(rect.height, this.maxHeight); 145 const currentX = Number.parseFloat(this.windowTarget.dataset.x || String(VIEWPORT_MARGIN)); 146 const currentY = Number.parseFloat(this.windowTarget.dataset.y || String(VIEWPORT_MARGIN)); 147 const x = Math.min(Math.max(VIEWPORT_MARGIN, currentX), Math.max(VIEWPORT_MARGIN, window.innerWidth - width - VIEWPORT_MARGIN)); 148 const y = Math.min(Math.max(VIEWPORT_MARGIN, currentY), Math.max(VIEWPORT_MARGIN, window.innerHeight - height - VIEWPORT_MARGIN)); 149 150 this.setGeometry({ x, y, width, height }); 151 } 152 153 setGeometry({ x, y, width, height }) { 154 const nextWidth = Math.round(Math.min(Math.max(this.minWidth, width), this.maxWidth)); 155 const nextHeight = Math.round(Math.min(Math.max(this.minHeight, height), this.maxHeight)); 156 157 this.windowTarget.style.width = `${nextWidth}px`; 158 this.windowTarget.style.height = `${nextHeight}px`; 159 this.setPosition(x, y, nextWidth, nextHeight); 160 } 161 162 setPosition(x, y, width = this.windowTarget.offsetWidth, height = this.windowTarget.offsetHeight) { 163 const maxX = Math.max(VIEWPORT_MARGIN, window.innerWidth - width - VIEWPORT_MARGIN); 164 const maxY = Math.max(VIEWPORT_MARGIN, window.innerHeight - height - VIEWPORT_MARGIN); 165 const nextX = Math.round(Math.min(Math.max(VIEWPORT_MARGIN, x), maxX)); 166 const nextY = Math.round(Math.min(Math.max(VIEWPORT_MARGIN, y), maxY)); 167 168 this.windowTarget.style.transform = `translate(${nextX}px, ${nextY}px)`; 169 this.windowTarget.dataset.x = String(nextX); 170 this.windowTarget.dataset.y = String(nextY); 171 } 172 173 startInteraction() { 174 this.windowTarget.classList.add('is-interacting'); 175 } 176 177 endInteraction() { 178 this.windowTarget.classList.remove('is-interacting'); 179 this.keepInViewport(); 180 } 181 182 get maxWidth() { 183 return Math.max(1, window.innerWidth - (VIEWPORT_MARGIN * 2)); 184 } 185 186 get maxHeight() { 187 return Math.max(1, window.innerHeight - (VIEWPORT_MARGIN * 2)); 188 } 189 190 get minWidth() { 191 return Math.min(MIN_WIDTH, this.maxWidth); 192 } 193 194 get minHeight() { 195 return Math.min(MIN_HEIGHT, this.maxHeight); 196 } 197}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.