mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-27 08:12:22 +01:00
Merge current dev and refresh PR #6740 from the 7011 integration
This commit is contained in:
+210
-14
@@ -22,7 +22,7 @@ import {
|
||||
} from './utils.js';
|
||||
import { state } from './state.js';
|
||||
import { getSettings } from '../appConfig.js';
|
||||
import { collapseSidebarToRail } from '../modalSnap.js';
|
||||
import { clampWindowLeft, collapseSidebarToRail, leftNavRight, safeInsetPx } from '../modalSnap.js';
|
||||
import { emailApiUrl } from '../emailShared.js';
|
||||
import {
|
||||
bindMenuDismiss,
|
||||
@@ -823,7 +823,7 @@ function _readCssPx(name) {
|
||||
}
|
||||
|
||||
function _emailSplitLeftEdge() {
|
||||
return _readCssPx('--icon-rail-w') + _readCssPx('--sidebar-w');
|
||||
return leftNavRight();
|
||||
}
|
||||
|
||||
function _setEmailDocumentSplit(leftEdge, emailWidth) {
|
||||
@@ -899,7 +899,8 @@ function _clearEmailDocumentSplit() {
|
||||
// rail. Used by the "try collapsing the sidebar first" path so we can decide
|
||||
// whether collapsing recovers enough room before minimizing email.
|
||||
function _emailSplitLeftEdgeIfSidebarCollapsed() {
|
||||
return _readCssPx('--icon-rail-w');
|
||||
const navOnRight = document.getElementById('sidebar')?.classList.contains('right-side');
|
||||
return safeInsetPx('left') + (navOnRight ? 0 : _readCssPx('--icon-rail-w'));
|
||||
}
|
||||
|
||||
function _hasDesktopRoomForEmailAndDocument(modal, opts = {}) {
|
||||
@@ -1122,7 +1123,175 @@ function _filenameFromContentDisposition(header) {
|
||||
return bare ? bare[1].trim() : '';
|
||||
}
|
||||
|
||||
export async function _exportSelectedAttachments() {
|
||||
async function _printSelectedEmails(messages) {
|
||||
if (messages.length > 20) throw new Error('Print / save PDF supports up to 20 emails at a time');
|
||||
if (typeof window.print !== 'function') throw new Error('Browser printing is unavailable');
|
||||
|
||||
const loaded = new Array(messages.length);
|
||||
let nextIndex = 0;
|
||||
const readWorker = async () => {
|
||||
while (nextIndex < messages.length) {
|
||||
const index = nextIndex++;
|
||||
const message = messages[index];
|
||||
try {
|
||||
const account = message.account_id ? `&account_id=${encodeURIComponent(message.account_id)}` : '';
|
||||
const url = `${API_BASE}/api/email/read/${encodeURIComponent(message.uid)}?folder=${encodeURIComponent(message.folder || 'INBOX')}${account}&mark_seen=false&full=true`;
|
||||
const response = await fetch(url, { credentials: 'same-origin' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
if (data?.error) throw new Error(data.error);
|
||||
loaded[index] = { ok: true, message, data };
|
||||
} catch (err) {
|
||||
loaded[index] = { ok: false, message, error: err?.message || 'Failed to load' };
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.all(Array.from({ length: Math.min(4, messages.length) }, readWorker));
|
||||
|
||||
const successful = loaded.filter(item => item && item.ok);
|
||||
const omitted = loaded.filter(item => !item || !item.ok);
|
||||
|
||||
if (successful.length === 0) {
|
||||
throw new Error('None of the selected emails could be loaded for printing');
|
||||
}
|
||||
|
||||
// Build sanitized HTML for the print frame -- scripts and event handlers
|
||||
// cannot execute inside the sandboxed iframe.
|
||||
const articles = [];
|
||||
if (omitted.length > 0) {
|
||||
const count = omitted.length;
|
||||
articles.push(
|
||||
`<div class="email-export-pdf-omitted-notice">Note: ${count} selected email${count === 1 ? ' was' : 's were'} unavailable and omitted from this print.</div>`
|
||||
);
|
||||
}
|
||||
|
||||
for (const { message, data } of successful) {
|
||||
const subject = String(data.subject || message.subject || '(no subject)');
|
||||
const from = data.from_name && data.from_address
|
||||
? `${data.from_name} <${data.from_address}>`
|
||||
: data.from_name || data.from_address;
|
||||
|
||||
const safeBody = new DOMParser().parseFromString(_safeRenderEmailBody(data), 'text/html');
|
||||
|
||||
// Replace interactive image placeholder components with clean print-safe notices
|
||||
safeBody.querySelectorAll('.email-inline-image-placeholder').forEach(ph => {
|
||||
const title = ph.querySelector('.email-inline-image-title')?.textContent?.trim();
|
||||
const ariaLabel = ph.getAttribute('aria-label')?.trim();
|
||||
const label = title || ariaLabel || '';
|
||||
const isGeneric = !label || /^inline image$|^remote image$|^image unavailable$|^image$/i.test(label);
|
||||
const span = safeBody.createElement('span');
|
||||
span.className = 'email-export-image-notice';
|
||||
span.textContent = isGeneric ? '[Image omitted]' : `[Image: ${label}]`;
|
||||
ph.replaceWith(span);
|
||||
});
|
||||
|
||||
// Replace any remaining raw <img> tags
|
||||
safeBody.querySelectorAll('img').forEach(img => {
|
||||
const alt = (img.getAttribute('alt') || img.getAttribute('title') || '').trim();
|
||||
const span = safeBody.createElement('span');
|
||||
span.className = 'email-export-image-notice';
|
||||
span.textContent = alt ? `[Image: ${alt}]` : '[Image omitted]';
|
||||
img.replaceWith(span);
|
||||
});
|
||||
|
||||
// Strip interactive controls (buttons, inputs, actions) from the print document
|
||||
safeBody.querySelectorAll('button, input, select, textarea, .email-inline-image-actions, .email-inline-image-btn').forEach(el => el.remove());
|
||||
|
||||
safeBody.querySelectorAll('details').forEach(detail => { detail.open = true; });
|
||||
const bodyHtml = safeBody.body.innerHTML;
|
||||
|
||||
const fieldParts = [];
|
||||
for (const [lbl, val] of [['From', from], ['To', data.to], ['Cc', data.cc], ['Date', data.date_display || data.date || message.date]]) {
|
||||
if (!val) continue;
|
||||
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
fieldParts.push(`<div class="email-export-pdf-field"><span class="email-export-pdf-label">${esc(lbl)}</span><span>${esc(val)}</span></div>`);
|
||||
}
|
||||
|
||||
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
articles.push(
|
||||
`<article class="email-export-pdf-message">` +
|
||||
`<h1>${esc(subject)}</h1>` +
|
||||
`<div class="email-export-pdf-fields">${fieldParts.join('')}</div>` +
|
||||
`<div class="email-export-pdf-body">${bodyHtml}</div>` +
|
||||
`</article>`
|
||||
);
|
||||
}
|
||||
|
||||
const printCss = [
|
||||
'html,body{background:#fff;color:#171717;font:12px/1.55 Arial,Helvetica,sans-serif;margin:0;padding:0}',
|
||||
'.email-export-pdf{max-width:794px;margin:0 auto;padding:38px;box-sizing:border-box}',
|
||||
'.email-export-pdf-message{page-break-after:always;break-after:page}',
|
||||
'.email-export-pdf-message:last-child{page-break-after:auto;break-after:auto}',
|
||||
'.email-export-pdf-message h1{margin:0 0 18px;font-size:20px;line-height:1.3;overflow-wrap:anywhere}',
|
||||
'.email-export-pdf-fields{margin:0 0 20px;padding:0 0 14px;border-bottom:1px solid #d8d8d8}',
|
||||
'.email-export-pdf-field{display:flex;gap:10px;margin:3px 0;overflow-wrap:anywhere}',
|
||||
'.email-export-pdf-label{width:54px;flex:0 0 54px;color:#666}',
|
||||
'.email-export-pdf-body{overflow-wrap:anywhere;word-break:break-word}',
|
||||
'.email-export-pdf-body img{max-width:100%}',
|
||||
'.email-export-pdf-body a{color:#135f9b;text-decoration:underline}',
|
||||
'.email-export-pdf-body blockquote{margin-left:12px;padding-left:10px;border-left:2px solid #ccc;color:#555}',
|
||||
'.email-export-pdf-body pre{white-space:pre-wrap}',
|
||||
'.email-inline-image-placeholder,.email-inline-image-actions,.email-inline-image-btn{display:none !important}',
|
||||
'.email-export-image-notice{display:inline-block;color:#666;font-style:italic;font-size:11px;margin:4px 0}',
|
||||
'.email-export-pdf-omitted-notice{margin:0 0 16px;padding:8px 12px;background:#f8f9fa;border:1px solid #e2e8f0;border-radius:4px;font-size:11px;color:#64748b}',
|
||||
'@page{margin:10mm}',
|
||||
].join('\n');
|
||||
|
||||
const safeBaseName = successful.length === 1
|
||||
? String(successful[0]?.data?.subject || successful[0]?.message?.subject || 'email').replace(/[^\p{L}\p{N} ._-]+/gu, '_').trim().slice(0, 100) || 'email'
|
||||
: 'selected-emails';
|
||||
|
||||
// Use a sandboxed same-origin iframe following the established document.js
|
||||
// Print / save PDF pattern (SAN-157). The browser opens a print dialog;
|
||||
// saving, cancellation and pagination belong to the browser.
|
||||
document.getElementById('email-browser-print-frame')?.remove();
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = 'email-browser-print-frame';
|
||||
frame.title = 'Email print view';
|
||||
frame.setAttribute('sandbox', 'allow-same-origin allow-modals');
|
||||
frame.style.cssText = 'position:fixed;left:-10000px;top:0;width:800px;height:600px;border:0;';
|
||||
document.body.appendChild(frame);
|
||||
|
||||
let cleanedUp = false;
|
||||
const cleanupFrame = () => {
|
||||
if (cleanedUp) return;
|
||||
cleanedUp = true;
|
||||
try {
|
||||
frame.remove();
|
||||
} catch (_) {}
|
||||
window.removeEventListener('afterprint', cleanupFrame);
|
||||
};
|
||||
|
||||
try {
|
||||
const printDoc = frame.contentDocument;
|
||||
printDoc.open();
|
||||
printDoc.write('<!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body></html>');
|
||||
printDoc.close();
|
||||
printDoc.title = safeBaseName;
|
||||
const printStyle = printDoc.createElement('style');
|
||||
printStyle.textContent = printCss;
|
||||
printDoc.head.appendChild(printStyle);
|
||||
const container = printDoc.createElement('div');
|
||||
container.className = 'email-export-pdf';
|
||||
container.innerHTML = articles.join('');
|
||||
printDoc.body.appendChild(container);
|
||||
await printDoc.fonts.ready;
|
||||
frame.contentWindow?.addEventListener('afterprint', cleanupFrame, { once: true });
|
||||
window.addEventListener('afterprint', cleanupFrame, { once: true });
|
||||
setTimeout(cleanupFrame, 60000);
|
||||
frame.contentWindow.focus();
|
||||
frame.contentWindow.print();
|
||||
if (omitted.length > 0) {
|
||||
const msg = `Prepared ${successful.length} email${successful.length === 1 ? '' : 's'} for printing; ${omitted.length} unavailable`;
|
||||
showToast(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
cleanupFrame();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function _exportSelectedBundle(kind) {
|
||||
const messages = _selectedEmailExportPayload();
|
||||
if (!messages.length) {
|
||||
showToast('Select emails first');
|
||||
@@ -1150,8 +1319,14 @@ export async function _exportSelectedAttachments() {
|
||||
if (deleteBtn) deleteBtn.disabled = true;
|
||||
if (cancelBtn) cancelBtn.disabled = true;
|
||||
if (selectAll) selectAll.disabled = true;
|
||||
if (countEl) countEl.textContent = `Exporting attachments from ${messages.length}…`;
|
||||
const res = await fetch(`${API_BASE}/api/email/attachments-download-bulk`, {
|
||||
const isEmailExport = kind === 'emails';
|
||||
const isPrintExport = kind === 'print';
|
||||
if (countEl) countEl.textContent = `${isPrintExport ? 'Preparing print' : 'Exporting'} ${isPrintExport ? 'for' : isEmailExport ? 'emails from' : 'attachments from'} ${messages.length}…`;
|
||||
if (isPrintExport) {
|
||||
await _printSelectedEmails(messages);
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`${API_BASE}/api/email/${isEmailExport ? 'messages' : 'attachments'}-download-bulk`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -1169,12 +1344,21 @@ export async function _exportSelectedAttachments() {
|
||||
throw new Error(detail || `HTTP ${res.status}`);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const filename = _filenameFromContentDisposition(res.headers.get('Content-Disposition')) || 'selected-email-attachments.zip';
|
||||
const filename = _filenameFromContentDisposition(res.headers.get('Content-Disposition'))
|
||||
|| (isEmailExport
|
||||
? (messages.length === 1 ? 'selected-email.eml' : 'selected-emails.zip')
|
||||
: 'selected-email-attachments.zip');
|
||||
_downloadBlob(blob, filename);
|
||||
showToast(`Exported attachments from ${messages.length} selected email${messages.length === 1 ? '' : 's'}`);
|
||||
const exported = Number(res.headers.get('X-Exported-Emails')) || messages.length;
|
||||
const skipped = Number(res.headers.get('X-Skipped-Emails')) || 0;
|
||||
showToast(skipped
|
||||
? `Exported ${exported} email${exported === 1 ? '' : 's'}; ${skipped} unavailable`
|
||||
: isEmailExport
|
||||
? `Exported ${exported} email${exported === 1 ? '' : 's'} as ${exported === 1 ? 'an .eml file' : '.eml files'}`
|
||||
: `Exported attachments from ${messages.length} selected email${messages.length === 1 ? '' : 's'}`);
|
||||
} catch (err) {
|
||||
console.error('Failed to export selected email attachments:', err);
|
||||
showToast(err?.message || 'Failed to export attachments');
|
||||
console.error(`Failed to export selected email ${kind}:`, err);
|
||||
showToast(err?.message || (kind === 'emails' ? 'Failed to export emails' : kind === 'print' ? 'Could not open browser print' : 'Failed to export attachments'));
|
||||
} finally {
|
||||
if (busySpinner) busySpinner.destroy();
|
||||
if (actionsBtn) {
|
||||
@@ -1190,6 +1374,18 @@ export async function _exportSelectedAttachments() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function _exportSelectedEmails() {
|
||||
return _exportSelectedBundle('emails');
|
||||
}
|
||||
|
||||
export async function _exportSelectedEmailPdf() {
|
||||
return _exportSelectedBundle('print');
|
||||
}
|
||||
|
||||
export async function _exportSelectedAttachments() {
|
||||
return _exportSelectedBundle('attachments');
|
||||
}
|
||||
|
||||
// URL-suffix helper — appends &account_id=... when an account is actively selected.
|
||||
// Every email route call in this file goes through here so switching accounts
|
||||
// is a single-variable flip.
|
||||
@@ -2242,7 +2438,7 @@ export function openEmailLibrary(opts = {}) {
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<button class="close-btn" id="email-lib-close" aria-label="Close">\u2716</button>
|
||||
<button class="close-btn" id="email-lib-close">\u2716</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body" style="display:flex;flex-direction:column;gap:10px;overflow:hidden;">
|
||||
@@ -2446,7 +2642,7 @@ export function openEmailLibrary(opts = {}) {
|
||||
requestAnimationFrame(() => {
|
||||
const w = content.offsetWidth;
|
||||
const refH = window.innerHeight * 0.85;
|
||||
content.style.left = Math.max(20, (window.innerWidth - w) / 2) + 'px';
|
||||
content.style.left = clampWindowLeft(Math.max(20, (window.innerWidth - w) / 2), w) + 'px';
|
||||
content.style.top = Math.max(20, (window.innerHeight - refH) / 2) + 'px';
|
||||
content.style.transform = 'none';
|
||||
});
|
||||
@@ -4825,7 +5021,7 @@ export async function _refreshUnreadBadge({ unreadCountOverride = null, preserve
|
||||
} catch (_) { _syncUnreadTabBadge(0); }
|
||||
}
|
||||
|
||||
export async function _loadEmails({ force = false, useCache = true, skipFastPath = false } = {}) {
|
||||
export async function _loadEmails({ force = false, useCache = true } = {}) {
|
||||
const seq = ++_libLoadSeq;
|
||||
state._libLoading = true;
|
||||
const accountAtStart = state._libAccountId || '';
|
||||
@@ -4923,7 +5119,7 @@ export async function _loadEmails({ force = false, useCache = true, skipFastPath
|
||||
const accountQS = accountAtStart ? `&account_id=${encodeURIComponent(accountAtStart)}` : '';
|
||||
const attQS = hasAttachmentsAtStart ? '&has_attachments=1' : '';
|
||||
const dateQS = `${dateFromAtStart ? `&date_from=${encodeURIComponent(dateFromAtStart)}` : ''}${dateToAtStart ? `&date_to=${encodeURIComponent(dateToAtStart)}` : ''}`;
|
||||
if (!cached && cacheable && !force && !skipFastPath) {
|
||||
if (!cached && cacheable && !force) {
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 450);
|
||||
try {
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
_animateEmailCardRemoval,
|
||||
_clearDoneResponseTagsLocal,
|
||||
_emailMutationQuery,
|
||||
_exportSelectedEmailPdf,
|
||||
_exportSelectedEmails,
|
||||
_exportSelectedAttachments,
|
||||
_findSiblingEmailCard,
|
||||
_fitEmailDropdown,
|
||||
@@ -585,7 +587,10 @@ export function _showBulkActionsMenu(anchor) {
|
||||
const _unreadIco = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>';
|
||||
const _doneIco = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
||||
const _exportIco = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>';
|
||||
const _emailIco = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>';
|
||||
const _pdfIco = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M8 15h8M8 18h5"/></svg>';
|
||||
const items = [
|
||||
{ label: 'Export Emails', icon: _exportIco, submenu: true },
|
||||
{ label: 'Export Attachments', icon: _exportIco, action: () => _exportSelectedAttachments() },
|
||||
{ label: 'Done', icon: _doneIco, action: () => _bulkAction('done') },
|
||||
{ label: 'Mark Read', icon: _readIco, action: () => _bulkAction('read') },
|
||||
@@ -594,8 +599,45 @@ export function _showBulkActionsMenu(anchor) {
|
||||
for (const a of items) {
|
||||
const it = document.createElement('div');
|
||||
it.className = 'dropdown-item-compact' + (a.danger ? ' dropdown-item-danger' : '');
|
||||
it.innerHTML = `<span class="dropdown-icon">${a.icon}</span><span>${a.label}</span>`;
|
||||
it.addEventListener('click', (e) => { e.stopPropagation(); close(); a.action(); });
|
||||
it.className += a.submenu ? ' email-bulk-submenu-trigger' : '';
|
||||
it.innerHTML = `<span class="dropdown-icon">${a.icon}</span><span>${a.label}</span>${a.submenu ? '<span style="margin-left:auto;opacity:0.55;">›</span>' : ''}`;
|
||||
it.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (a.submenu) {
|
||||
dropdown.replaceChildren();
|
||||
const back = document.createElement('div');
|
||||
back.className = 'dropdown-item-compact email-bulk-submenu-back';
|
||||
back.innerHTML = '<span style="opacity:.65;margin-right:4px;">‹</span><span>Export Emails</span>';
|
||||
back.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
close();
|
||||
_showBulkActionsMenu(anchor);
|
||||
});
|
||||
dropdown.appendChild(back);
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'dropdown-divider';
|
||||
dropdown.appendChild(divider);
|
||||
const options = [
|
||||
{ label: 'Email file (.eml)', icon: _emailIco, action: _exportSelectedEmails },
|
||||
{ label: 'Print / save PDF', icon: _pdfIco, action: _exportSelectedEmailPdf },
|
||||
];
|
||||
for (const option of options) {
|
||||
const child = document.createElement('div');
|
||||
child.className = 'dropdown-item-compact';
|
||||
child.innerHTML = `<span class="dropdown-icon">${option.icon}</span><span>${option.label}</span>`;
|
||||
child.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
close();
|
||||
option.action();
|
||||
});
|
||||
dropdown.appendChild(child);
|
||||
}
|
||||
_fitEmailDropdown(dropdown, anchor.getBoundingClientRect());
|
||||
return;
|
||||
}
|
||||
close();
|
||||
a.action();
|
||||
});
|
||||
dropdown.appendChild(it);
|
||||
}
|
||||
// Mobile-only Cancel — matches the per-card and sidebar dropdowns.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import spinnerModule from '../spinner.js';
|
||||
import * as Modals from '../modalManager.js';
|
||||
import { clampWindowLeft } from '../modalSnap.js';
|
||||
import { showToast } from '../ui.js?v=20260916largetoolscroll1';
|
||||
import { state } from './state.js';
|
||||
import { _esc, _extractName, _renderEmailSummaryError } from './utils.js';
|
||||
@@ -428,7 +429,7 @@ export async function _openEmailWindow(em, folder) {
|
||||
requestAnimationFrame(() => {
|
||||
const w = content.offsetWidth, h = content.offsetHeight;
|
||||
const off = (_emailWindowSeq % 6) * 28;
|
||||
content.style.left = Math.max(20, (window.innerWidth - w) / 2 + off) + 'px';
|
||||
content.style.left = clampWindowLeft(Math.max(20, (window.innerWidth - w) / 2 + off), w) + 'px';
|
||||
content.style.top = Math.max(20, (window.innerHeight - h) / 3 + off) + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user