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 { useEffect, useRef, useState } from 'react';
|
||||||
import { api } from '../../lib/api.js';
|
import { api } from '../../lib/api.js';
|
||||||
|
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
||||||
|
|
||||||
const INITIAL = {
|
const INITIAL = {
|
||||||
subject: 'Actualités du Business Club SLUC Nancy',
|
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",
|
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() {
|
export function EmailCreatorTab() {
|
||||||
const [form, setForm] = useState(INITIAL);
|
const [form, setForm] = useState(INITIAL);
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
@@ -69,17 +49,7 @@ export function EmailCreatorTab() {
|
|||||||
|
|
||||||
const copyEmail = async () => {
|
const copyEmail = async () => {
|
||||||
try {
|
try {
|
||||||
const fragment = bodyFragment(data.html);
|
await copyRichEmail(data.html, `${form.subject}\n\n${form.body}`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
flash('email');
|
flash('email');
|
||||||
} catch {
|
} catch {
|
||||||
setError('La copie enrichie a échoué dans ce navigateur. Utilisez le téléchargement HTML.');
|
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 { useEffect, useRef, useState } from 'react';
|
||||||
import { api, dateParts, seasonLabel, statutLabel } from '../../lib/api.js';
|
import { api, dateParts, seasonLabel, statutLabel } from '../../lib/api.js';
|
||||||
|
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
||||||
import { useAuth } from '../../lib/AuthContext.jsx';
|
import { useAuth } from '../../lib/AuthContext.jsx';
|
||||||
import Modal from '../Modal.jsx';
|
import Modal from '../Modal.jsx';
|
||||||
import ImageSlot from '../ImageSlot.jsx';
|
import ImageSlot from '../ImageSlot.jsx';
|
||||||
@@ -319,34 +320,6 @@ function escHtml(v) {
|
|||||||
return String(v ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
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
|
// Composer + preview of the ready-to-send HTML invitation email generated
|
||||||
// server-side for one rencontre. Every text zone (salutation, intro,
|
// server-side for one rencontre. Every text zone (salutation, intro,
|
||||||
// conclusion, signature) is editable — the preview regenerates as the
|
// conclusion, signature) is editable — the preview regenerates as the
|
||||||
@@ -402,17 +375,7 @@ function EmailModal({ renc, onClose }) {
|
|||||||
const copyEmail = async () => {
|
const copyEmail = async () => {
|
||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const fragment = emailBodyFragment(data.html);
|
await copyRichEmail(data.html, `${data.subject}\n${data.inscriptionUrl}`);
|
||||||
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);
|
|
||||||
}
|
|
||||||
flash('email');
|
flash('email');
|
||||||
} catch {
|
} catch {
|
||||||
setError('Copie impossible dans ce navigateur — utilisez le téléchargement.');
|
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