Fix Outlook email copy formatting
This commit is contained in:
1 parent
f00fdffac6
commit
ec75ee32da
3 files changed
+65
-71
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { api } from '../../lib/api.js';
|
||||
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
||||
|
||||
const INITIAL = {
|
||||
subject: 'Actualités du Business Club SLUC Nancy',
|
||||
@@ -11,27 +12,6 @@ const INITIAL = {
|
||||
signature: "À très bientôt,\nL'équipe du Business Club SLUC Nancy",
|
||||
};
|
||||
|
||||
function bodyFragment(html) {
|
||||
return new DOMParser().parseFromString(html, 'text/html').body.innerHTML.trim();
|
||||
}
|
||||
|
||||
function legacyCopy(html) {
|
||||
const holder = document.createElement('div');
|
||||
holder.contentEditable = 'true';
|
||||
holder.style.cssText = 'position:fixed;left:-10000px;top:0;width:700px;opacity:0;pointer-events:none;';
|
||||
holder.innerHTML = html;
|
||||
document.body.appendChild(holder);
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(holder);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
const copied = document.execCommand('copy');
|
||||
selection.removeAllRanges();
|
||||
holder.remove();
|
||||
if (!copied) throw new Error('Rich copy failed');
|
||||
}
|
||||
|
||||
export function EmailCreatorTab() {
|
||||
const [form, setForm] = useState(INITIAL);
|
||||
const [data, setData] = useState(null);
|
||||
@@ -69,17 +49,7 @@ export function EmailCreatorTab() {
|
||||
|
||||
const copyEmail = async () => {
|
||||
try {
|
||||
const fragment = bodyFragment(data.html);
|
||||
if (navigator.clipboard?.write && window.ClipboardItem) {
|
||||
await navigator.clipboard.write([
|
||||
new window.ClipboardItem({
|
||||
'text/html': new Blob([fragment], { type: 'text/html' }),
|
||||
'text/plain': new Blob([`${form.subject}\n\n${form.body}`], { type: 'text/plain' }),
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
legacyCopy(fragment);
|
||||
}
|
||||
await copyRichEmail(data.html, `${form.subject}\n\n${form.body}`);
|
||||
flash('email');
|
||||
} catch {
|
||||
setError('La copie enrichie a échoué dans ce navigateur. Utilisez le téléchargement HTML.');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { api, dateParts, seasonLabel, statutLabel } from '../../lib/api.js';
|
||||
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
||||
import { useAuth } from '../../lib/AuthContext.jsx';
|
||||
import Modal from '../Modal.jsx';
|
||||
import ImageSlot from '../ImageSlot.jsx';
|
||||
@@ -319,34 +320,6 @@ function escHtml(v) {
|
||||
return String(v ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// Clipboard HTML must be a fragment, not a second complete document. Outlook
|
||||
// otherwise rebuilds the pasted <body> and drops part of the outer layout.
|
||||
function emailBodyFragment(html) {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return doc.body.innerHTML.trim();
|
||||
}
|
||||
|
||||
// Older browsers do not expose ClipboardItem. A real rich-text selection is
|
||||
// still preferable to copying the HTML source as plain text.
|
||||
function legacyCopyRichHtml(html) {
|
||||
const holder = document.createElement('div');
|
||||
holder.setAttribute('contenteditable', 'true');
|
||||
holder.setAttribute('aria-hidden', 'true');
|
||||
holder.style.cssText = 'position:fixed;left:-10000px;top:0;width:700px;opacity:0;pointer-events:none;';
|
||||
holder.innerHTML = html;
|
||||
document.body.appendChild(holder);
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(holder);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
const copied = document.execCommand('copy');
|
||||
selection.removeAllRanges();
|
||||
holder.remove();
|
||||
if (!copied) throw new Error('Rich HTML copy failed');
|
||||
}
|
||||
|
||||
// Composer + preview of the ready-to-send HTML invitation email generated
|
||||
// server-side for one rencontre. Every text zone (salutation, intro,
|
||||
// conclusion, signature) is editable — the preview regenerates as the
|
||||
@@ -402,17 +375,7 @@ function EmailModal({ renc, onClose }) {
|
||||
const copyEmail = async () => {
|
||||
setError('');
|
||||
try {
|
||||
const fragment = emailBodyFragment(data.html);
|
||||
if (navigator.clipboard?.write && window.ClipboardItem) {
|
||||
await navigator.clipboard.write([
|
||||
new window.ClipboardItem({
|
||||
'text/html': new Blob([fragment], { type: 'text/html' }),
|
||||
'text/plain': new Blob([`${data.subject}\n${data.inscriptionUrl}`], { type: 'text/plain' }),
|
||||
}),
|
||||
]);
|
||||
} else {
|
||||
legacyCopyRichHtml(fragment);
|
||||
}
|
||||
await copyRichEmail(data.html, `${data.subject}\n${data.inscriptionUrl}`);
|
||||
flash('email');
|
||||
} catch {
|
||||
setError('Copie impossible dans ce navigateur — utilisez le téléchargement.');
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
function emailBodyFragment(html) {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
return doc.body.innerHTML.trim();
|
||||
}
|
||||
|
||||
function copyRenderedSelection(html) {
|
||||
const holder = document.createElement('div');
|
||||
holder.setAttribute('contenteditable', 'true');
|
||||
holder.setAttribute('aria-hidden', 'true');
|
||||
holder.style.cssText = [
|
||||
'position:fixed',
|
||||
'left:-10000px',
|
||||
'top:0',
|
||||
'width:700px',
|
||||
'background:#F2EEE8',
|
||||
'pointer-events:none',
|
||||
'z-index:-1',
|
||||
].join(';');
|
||||
holder.innerHTML = html;
|
||||
document.body.appendChild(holder);
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
|
||||
try {
|
||||
holder.focus();
|
||||
range.selectNodeContents(holder);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
if (!document.execCommand('copy')) {
|
||||
throw new Error("La copie de l'e-mail a échoué.");
|
||||
}
|
||||
} finally {
|
||||
selection.removeAllRanges();
|
||||
holder.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyRichEmail(html, plainText = '') {
|
||||
const fragment = emailBodyFragment(html);
|
||||
|
||||
try {
|
||||
// Outlook conserve beaucoup mieux la mise en forme lorsque le presse-papiers
|
||||
// provient d'une sélection réellement rendue par le navigateur.
|
||||
copyRenderedSelection(fragment);
|
||||
return;
|
||||
} catch (selectionError) {
|
||||
if (!navigator.clipboard?.write || !window.ClipboardItem) {
|
||||
throw selectionError;
|
||||
}
|
||||
}
|
||||
|
||||
// Repli pour les navigateurs qui refusent execCommand('copy').
|
||||
await navigator.clipboard.write([
|
||||
new window.ClipboardItem({
|
||||
'text/html': new Blob([fragment], { type: 'text/html' }),
|
||||
'text/plain': new Blob([plainText], { type: 'text/plain' }),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
Reference in new issue
Block a user