PageSourceSearch

https://www.buscyclette.fr/assets/controllers/rental/document_viewer_controller-lTFMzo6.js

js buscyclette.fr collected 2026-10-08 23:33:46 UTC 6,843 bytes, 197 lines download raw bytes

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.