PageSourceSearch

https://freelance.ru/assets/db7ac538/reply-attachments-preview.js?v=1790077317

js freelance.ru collected 2026-09-24 08:27:03 UTC 9,817 bytes, 226 lines download raw bytes

1document.addEventListener('DOMContentLoaded', function () {
2    function escapeHtml(value) {
3        return String(value || '')
4            .replace(/&/g, '&')
5            .replace(/</g, '&lt;')
6            .replace(/>/g, '&gt;')
7            .replace(/"/g, '&quot;')
8            .replace(/'/g, '&#039;');
9    }
10
11    function collectItems(previewLink) {
12        const grid = previewLink.closest('.reply-attachments-grid');
13        const links = grid
14            ? Array.from(grid.querySelectorAll('.reply-attachment-link[data-preview-type]'))
15            : [previewLink];
16        const items = links.map(function (link) {
17            const tile = link.closest('.reply-attachment-tile');
18            const downloadLink = tile ? tile.querySelector('.reply-attachment-download') : null;
19
20            return {
21                type: link.getAttribute('data-preview-type') || 'document',
22                url: link.getAttribute('data-preview-url') || '',
23                name: link.getAttribute('data-preview-name') || 'Файл',
24                ext: link.getAttribute('data-preview-ext') || '',
25                downloadUrl: downloadLink ? (downloadLink.getAttribute('href') || '') : (link.getAttribute('data-preview-url') || ''),
26            };
27        });
28
29        return {
30            items: items,
31            index: Math.max(0, links.indexOf(previewLink)),
32        };
33    }
34
35    function ensureModal() {
36        let modal = document.getElementById('replyAttachmentPreviewModal');
37        if (modal) {
38            return modal;
39        }
40
41        modal = document.createElement('div');
42        modal.id = 'replyAttachmentPreviewModal';
43        modal.className = 'reply-attachment-modal';
44        modal.innerHTML = `
45            <div class="reply-attachment-modal-backdrop" data-close="1"></div>
46            <div class="reply-attachment-modal-dialog" role="dialog" aria-modal="true">
47                <div class="reply-attachment-modal-top">
48                    <div class="reply-attachment-modal-title"></div>
49                    <div class="reply-attachment-modal-actions">
50                        <a class="reply-attachment-modal-download" href="#" target="_blank" rel="noopener" download aria-label="Скачать">
51                            <i class="fa fa-download" aria-hidden="true"></i>
52                        </a>
53                        <button type="button" class="reply-attachment-modal-close" data-close="1" aria-label="Закрыть">
54                            <i class="fa fa-times" aria-hidden="true"></i>
55                        </button>
56                    </div>
57                </div>
58                <button type="button" class="reply-attachment-modal-nav is-prev" data-nav="-1" aria-label="Предыдущее">
59                    <i class="fa fa-angle-left" aria-hidden="true"></i>
60                </button>
61                <button type="button" class="reply-attachment-modal-nav is-next" data-nav="1" aria-label="Следующее">
62                    <i class="fa fa-angle-right" aria-hidden="true"></i>
63                </button>
64                <div class="reply-attachment-modal-body"></div>
65            </div>
66        `;
67        document.body.appendChild(modal);
68        return modal;
69    }
70
71    function renderCurrent(modal) {
72        if (!modal || !modal._replyAttachmentState || !modal._replyAttachmentState.items.length) {
73            return;
74        }
75
76        const state = modal._replyAttachmentState;
77        const item = state.items[state.index];
78        const body = modal.querySelector('.reply-attachment-modal-body');
79        const title = modal.querySelector('.reply-attachment-modal-title');
80        const downloadButton = modal.querySelector('.reply-attachment-modal-download');
81        const prevButton = modal.querySelector('.reply-attachment-modal-nav.is-prev');
82        const nextButton = modal.querySelector('.reply-attachment-modal-nav.is-next');
83        const safeName = escapeHtml(item.name);
84        const safeExt = escapeHtml(item.ext);
85
86        if (!body || !title || !downloadButton) {
87            return;
88        }
89
90        title.textContent = item.name;
91
92        const dlIcon = downloadButton.querySelector('i');
93        if (item.type === 'portfolio') {
94            downloadButton.setAttribute('href', item.downloadUrl || '#');
95            downloadButton.removeAttribute('download');
96            if (dlIcon) dlIcon.className = 'fa fa-external-link';
97            downloadButton.setAttribute('aria-label', 'Открыть ' + item.name);
98        } else {
99            downloadButton.setAttribute('href', item.downloadUrl || item.url || '#');
100            downloadButton.setAttribute('download', '');
101            if (dlIcon) dlIcon.className = 'fa fa-download';
102            downloadButton.setAttribute('aria-label', 'Скачать ' + item.name);
103        }
104
105        if (item.type === 'image') {
106            body.innerHTML = `
107                <div class="reply-attachment-preview-image-wrap">
108                    <img src="${item.url}" alt="${safeName}" class="reply-attachment-preview-image">
109                </div>
110            `;
111        } else if (item.type === 'portfolio') {
112            if (item.url) {
113                body.innerHTML = `
114                    <div class="reply-attachment-preview-portfolio">
115                        <img src="${item.url}" alt="${safeName}" class="reply-attachment-preview-portfolio-img">
116                        <div class="reply-attachment-preview-portfolio-foot">
117                            <span class="reply-attachment-preview-portfolio-bar-name">
118                                <i class="fa fa-briefcase" aria-hidden="true"></i> ${safeName}
119                            </span>
120                            <a class="btn btn-default btn-xs" href="${item.downloadUrl}" target="_blank" rel="noopener">Открыть проект</a>
121                        </div>
122                    </div>
123                `;
124            } else {
125                body.innerHTML = `
126                    <div class="reply-attachment-preview-doc">
127                        <div class="reply-attachment-preview-doc-icon"><i class="fa fa-briefcase" aria-hidden="true"></i></div>
128                        <div class="reply-attachment-preview-doc-name">${safeName}</div>
129                        <a class="btn btn-default btn-sm m-t-1" href="${item.downloadUrl}" target="_blank" rel="noopener">Открыть проект</a>
130                    </div>
131                `;
132            }
133        } else {
134            body.innerHTML = `
135                <div class="reply-attachment-preview-doc">
136                    <div class="reply-attachment-preview-doc-icon"><i class="fa fa-file-o" aria-hidden="true"></i></div>
137                    <div class="reply-attachment-preview-doc-name">${safeName}</div>
138                    <div class="reply-attachment-preview-doc-ext">${safeExt}</div>
139                    <a class="btn btn-default btn-sm m-t-1" href="${item.url}" target="_blank" rel="noopener">Открыть</a>
140                </div>
141            `;
142        }
143
144        if (prevButton && nextButton) {
145            const hideNav = state.items.length < 2;
146            prevButton.classList.toggle('is-hidden', hideNav);
147            nextButton.classList.toggle('is-hidden', hideNav);
148        }
149    }
150
151    function step(modal, delta) {
152        if (!modal || !modal._replyAttachmentState || !modal._replyAttachmentState.items.length) {
153            return;
154        }
155
156        const state = modal._replyAttachmentState;
157        const length = state.items.length;
158        state.index = (state.index + delta + length) % length;
159        renderCurrent(modal);
160    }
161
162    function closeModal(modal) {
163        if (!modal) {
164            return;
165        }
166        modal.classList.remove('is-visible');
167        document.body.classList.remove('reply-attachment-modal-open');
168    }
169
170    document.addEventListener('click', function (event) {
171        const previewLink = event.target.closest('.reply-attachment-link');
172        const modal = document.getElementById('replyAttachmentPreviewModal');
173
174        if (previewLink) {
175            const previewType = previewLink.getAttribute('data-preview-type');
176            // Без типа или портфолио — не перехватываем: href откроется нативно
177            if (!previewType || previewType === 'portfolio') {
178                return;
179            }
180            event.preventDefault();
181            const currentModal = ensureModal();
182            currentModal._replyAttachmentState = collectItems(previewLink);
183            renderCurrent(currentModal);
184            currentModal.classList.add('is-visible');
185            document.body.classList.add('reply-attachment-modal-open');
186            return;
187        }
188
189        if (modal && event.target.closest('[data-nav]')) {
190            event.preventDefault();
191            const navControl = event.target.closest('[data-nav]');
192            const delta = parseInt(navControl.getAttribute('data-nav') || '0', 10);
193            if (delta) {
194                step(modal, delta);
195            }
196            return;
197        }
198
199        if (modal && event.target.closest('[data-close="1"]')) {
200            closeModal(modal);
201        }
202    });
203
204    document.addEventListener('keydown', function (event) {
205        const modal = document.getElementById('replyAttachmentPreviewModal');
206        if (!modal || !modal.classList.contains('is-visible')) {
207            return;
208        }
209
210        if (event.key === 'Escape') {
211            closeModal(modal);
212            return;
213        }
214
215        if (event.key === 'ArrowLeft') {
216            event.preventDefault();
217            step(modal, -1);
218            return;
219        }
220
221        if (event.key === 'ArrowRight') {
222            event.preventDefault();
223            step(modal, 1);
224        }
225    });
226});

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.