From 52d7c06cbf7831924a10d21f05a735fa885aaa56 Mon Sep 17 00:00:00 2001 From: Reinhold Kainhofer <office@open-tools.net> Date: Sun, 5 Jan 2025 02:17:00 +0100 Subject: [PATCH] Implement inserting placeholders, add alert box --- manifest.xml | 2 +- src/taskpane/taskpane.css | 40 ++++- src/taskpane/taskpane.html | 11 +- src/taskpane/taskpane.js | 338 +++++++++++++++++++++++++++++-------- 4 files changed, 317 insertions(+), 74 deletions(-) diff --git a/manifest.xml b/manifest.xml index 780a7cf..7c035db 100644 --- a/manifest.xml +++ b/manifest.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp"> <Id>8df42c88-61ce-4c8a-aa94-a46d56cbf46c</Id> - <Version>0.1.0.0</Version> + <Version>1.0.0.0</Version> <ProviderName>Open Tools</ProviderName> <DefaultLocale>en-US</DefaultLocale> <DisplayName DefaultValue="Outlook MailMerge"/> diff --git a/src/taskpane/taskpane.css b/src/taskpane/taskpane.css index 8ac6eb0..b32dd69 100644 --- a/src/taskpane/taskpane.css +++ b/src/taskpane/taskpane.css @@ -28,4 +28,42 @@ body { padding: 8px; text-align: left; } - \ No newline at end of file + + + /* Container for the message */ +.message-container { + display: none; /* Hidden by default */ + position: fixed; + top: 20px; + right: 20px; + padding: 15px; + background-color: #f44336; /* Red background for error messages */ + color: white; + border-radius: 5px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + z-index: 1000; + max-width: 300px; + font-family: Arial, sans-serif; + transition: opacity 0.3s ease; +} + +/* Close button styles */ +.close-button { + position: absolute; + top: 5px; + right: 5px; + font-size: 20px; + cursor: pointer; + color: white; +} + +/* Add animation for showing and hiding */ +.message-container.show { + display: block; + opacity: 1; +} + +.message-container.hide { + opacity: 0; + display: none; +} \ No newline at end of file diff --git a/src/taskpane/taskpane.html b/src/taskpane/taskpane.html index 070a16d..e2f2d4f 100644 --- a/src/taskpane/taskpane.html +++ b/src/taskpane/taskpane.html @@ -10,7 +10,10 @@ <div name="background"> <p>This add-on provides serial mail with individual attachments. Data is provided by an Excel table, Individual fields in the text can be inserted as {%fieldname%}.</p> - <p class="label"></p> + <div id="messageDiv" class="message-container"> + <span id="closeMessageButton" class="close-button">×</span> + <p id="messageText"></p> + </div> <fieldset> <legend>1. Select data source (Excel file):</legend> @@ -21,7 +24,10 @@ <fieldset> <legend>2. E-Mail - Placeholder fields:</legend> - <div class="placeholder_select">Placeholder: <select id="placeholderSelect" disabled></select><input id="insertPlaceholder" /></div> + <label for="placeholderSelect">Placeholder:</label> + <select id="placeholderSelect" disabled></select> + <button id="placeholderInsertButton">Insert</button> + <div id="placeholderCopyLabel">Copy</div> </fieldset> <fieldset> @@ -47,6 +53,7 @@ <div id="dataPreview"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> + <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script> <script src="taskpane.js"></script> </div> </body> diff --git a/src/taskpane/taskpane.js b/src/taskpane/taskpane.js index 002b7fe..4603f55 100644 --- a/src/taskpane/taskpane.js +++ b/src/taskpane/taskpane.js @@ -1,64 +1,75 @@ /* - * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. - * See LICENSE in the project root for license information. + * Copyright (c) 2025 Reinhold Kainhofer, Open Tools, office@open-tools.net */ -/* global document, Office */ - - -/* -Office.onReady(() => { - // Office Add-in is ready - console.log("Add-in is ready."); -}); - -Office.onReady((info) => { +// Ensure the Office.js library is loaded the office context is fully initialized! +Office.onReady(function (info) { + // Office is ready if (info.host === Office.HostType.Outlook) { - document.getElementById("sideload-msg").style.display = "none"; - document.getElementById("app-body").style.display = "flex"; - document.getElementById("run").onclick = run; + // Now you can safely use Office.context.mailbox + Office.context.mailbox.item.body.getTypeAsync((result) => { + if (result.status === Office.AsyncResultStatus.Succeeded) { + console.log(result.value); // the body type (e.g., Html or Text) + } else { + console.error("Failed to get body type:", result.error); + } + }); } }); -export async function run() { - // Insert your Outlook code here - const item = Office.context.mailbox.item; - let insertAt = document.getElementById("item-subject"); - let label = document.createElement("b").appendChild(document.createTextNode("Subject: ")); - insertAt.appendChild(label); - insertAt.appendChild(document.createElement("br")); - insertAt.appendChild(document.createTextNode(item.subject)); - insertAt.appendChild(document.createElement("br")); -}; -*/ - -const fileInput = document.getElementById("excelFileInput"); -const worksheetSelect = document.getElementById("worksheetSelect"); -const placeholderSelect = document.getElementById("placeholderSelect"); -const attachmentColumnSelect = document.getElementById("attachmentColumnSelect"); -const appendExtensionCheckbox = document.getElementById("appendExtensionCheckbox"); -const fileExtensionInput = document.getElementById("fileExtensionInput"); -const attachmentFolderButton = document.getElementById('selectAttachmentFolderButton'); -const attachmentFolderSpan = document.getElementById('selectedAttachmentFolder'); - let workbook = null; +// Create a global EventTarget instance +const eventBus = new EventTarget(); + + +function displayMessage(message) { + const messageDiv = document.getElementById("messageDiv"); + const messageText = document.getElementById("messageText"); + const closeButton = document.getElementById("closeMessageButton"); + + // Set the message text + messageText.textContent = message; + + // Show the message + messageDiv.classList.add("show"); + messageDiv.classList.remove("hide"); + + setTimeout(() => { + messageDiv.classList.add("hide"); + setTimeout(() => { + messageDiv.classList.remove("show", "hide"); + }, 300); + }, 5000); // Hide the message after 5 seconds + + // Close button functionality + closeButton.addEventListener("click", function() { + messageDiv.classList.add("hide"); + setTimeout(() => { + messageDiv.classList.remove("show", "hide"); + }, 300); // Remove the show and hide classes after the fade-out animation + }); -fileInput.addEventListener("change", handleFile); +} + + + + +/*********************************************************** + * 1. Select data source (Excel worksheet, CSV file etc.) + * + * After selecting the file, potentially multiple sheets / tables are available for selection + * + ***********************************************************/ + +const worksheetSelect = document.getElementById("worksheetSelect"); + +document.getElementById("excelFileInput").addEventListener("change", handleFile); worksheetSelect.addEventListener("change", handleWorksheetSelection); -attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection); -appendExtensionCheckbox.addEventListener("change", () => { - fileExtensionInput.disabled = !appendExtensionCheckbox.checked; -}); -attachmentFolderButton.addEventListener("click", async () => { - try { - const directoryHandle = await window.showDirectoryPicker(); - attachmentFolderSpan.textContent = `Selected folder: ${directoryHandle.name}`; - console.log("Selected folder:", directoryHandle); - } catch (err) { - console.error("Folder selection cancelled or failed", err); - } -}); + +/* + * Read the selected file (Excel, csv) into memory + */ function handleFile(event) { const file = event.target.files[0]; if (!file) return; @@ -66,26 +77,25 @@ function handleFile(event) { const reader = new FileReader(); reader.onload = function (e) { const data = new Uint8Array(e.target.result); + const binary = Array.from(data).map(byte => String.fromCharCode(byte)).join(''); const fileType = file.name.split(".").pop().toLowerCase(); // Determine the file type and parse accordingly - const options = { type: "array" }; if (fileType === "xlsb") { - options.type = "binary"; + workbook = XLSX.read(binary, { type: "binary" }); + } else { + workbook = XLSX.read(data, { type: "array" }); } - workbook = XLSX.read(data, options); populateWorksheetDropdown(); }; - const fileType = file.name.split(".").pop().toLowerCase(); - if (fileType === "xlsb") { - reader.readAsBinaryString(file); - } else { - reader.readAsArrayBuffer(file); - } + reader.readAsArrayBuffer(file); } +/* + * Make the list of worksheets selectable in a combo box and auto-select the first one + */ function populateWorksheetDropdown() { worksheetSelect.innerHTML = ""; worksheetSelect.disabled = false; @@ -112,28 +122,190 @@ function handleWorksheetSelection(event) { // Populate the placeholders combobox and the attachments column combo const worksheet = workbook.Sheets[sheetName]; const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); + const columns = jsonData[0] || []; - populateColumnSelect(jsonData[0] || []); - displayData(jsonData); // TODO: Where/how shall we display the table data? + const dataReadyEvent = new CustomEvent("dataReady", { + detail: { + data: jsonData, // Pass the loaded data as part of the event + columns: columns, // Pass the availabe column names + } + }); + eventBus.dispatchEvent(dataReadyEvent); } -function populateColumnSelect(columns) { + + + +/*********************************************************** + * 2. E-Mail Contents - Allow inserting placeholders from the available column + * + * After selecting the file and sheet, a combobox with all available placeholders is provided + * Next to is is a "Insert" button, which inserts the placeholder of the form {%fieldname%} + * into the mail at the current position. For Subject, To/CC/BCC we cannot automatically insert, + * but at least provide a copy button to copy the placeholder into the clipboard (TODO!) + * + ***********************************************************/ + +const placeholderSelect = document.getElementById("placeholderSelect"); +const placeholderInsertButton = document.getElementById("placeholderInsertButton"); +const placeholderCopyLabel = document.getElementById("placeholderCopyLabel"); + +// Attach event listener to the previous controls / eventbus +placeholderSelect.addEventListener("change", enablePlaceholderInsert); +placeholderInsertButton.addEventListener("click", insertPlaceholder); +placeholderCopyLabel.addEventListener("click", copyPlaceholderToClipboard) +eventBus.addEventListener("dataReady", populatePlaceholderSelect); + +function populatePlaceholderSelect(event) { + const columns = event.detail.columns; placeholderSelect.innerHTML = ""; + columns.forEach((column, index) => { + const option = document.createElement("option"); + option.value = index; + option.textContent = column || `Column ${index + 1}`; + placeholderSelect.appendChild(option); + }); + placeholderSelect.disabled = false; + placeholderSelect.dispatchEvent(new Event("change")); +} + +function enablePlaceholderInsert(event) { + const placeholder = event.target.value; + placeholderInsertButton.disabled = !placeholder; +} + + + +// Function to insert placeholder +async function insertPlaceholder() { + try { + // Get the selected field from the combo box +// const selectedField = placeholderSelect.value; + const selectedText = placeholderSelect.options[placeholderSelect.selectedIndex].text; + + if (!selectedText) { + displayMessage("Please select a field before inserting."); + return; + } + + const placeholder = `{%${selectedText}%}`; + + // Use the Office.js API to get the current item + await Office.context.mailbox.item.body.getTypeAsync(async (result) => { + if (result.status === Office.AsyncResultStatus.Succeeded) { + const bodyType = result.value; + + if (bodyType === Office.MailboxEnums.BodyType.Html) { + // Insert HTML placeholder into the body + await Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Html } ); + } else if (bodyType === Office.MailboxEnums.BodyType.Text) { + // Insert plain text placeholder into the body + await Office.context.mailbox.item.body.setSelectedDataAsync(placeholder, { coercionType: Office.CoercionType.Text } ); + } + } + }); + } catch (error) { + console.error("Error inserting placeholder:", error); + displayMessage("There was an error inserting the placeholder. Please try again."); + } +} + + +/* + * Function to insert the placeholder into other fields + * -> TODO: Add a dropdown to the "Insert" button to append to Subject/To/CC/BCC alternatively! + */ + +async function insertIntoField(field, placeholder) { + try { + const currentValue = await Office.context.mailbox.item[field].getAsync(); + if (currentValue.status === Office.AsyncResultStatus.Succeeded) { + const updatedValue = currentValue.value + placeholder; + await Office.context.mailbox.item[field].setAsync(updatedValue); + } + } catch (error) { + console.error(`Error inserting into ${field}:`, error); + } +} + + +// Example usage: +// insertIntoField('subject', '{%fieldname%}'); +// insertIntoField('to', '{%fieldname%}'); + + + +function copyPlaceholderToClipboard() { + //const selectedField = placeholderSelect.value; + const selectedText = placeholderSelect.options[placeholderSelect.selectedIndex].text; + + if (!selectedText) { + displayMessage("Please select a field before copying."); + return; + } + + const placeholder = `{%${selectedText}%}`; + navigator.clipboard.writeText(placeholder).then(() => { + displayMessage("Copied to clipboard!"); + }) + .catch((err) => { + console.error("Error copying to clipboard:", err); + }); +} + + + + +/*********************************************************** + * 3. Attachments handling + * + * Provide a combobox to select the column for individual attachments, + * optionally appends a file extension (e.g. "pdf") and select + * a folder where the attachments are stored. + * + ***********************************************************/ + +const attachmentColumnSelect = document.getElementById("attachmentColumnSelect"); +const appendExtensionCheckbox = document.getElementById("appendExtensionCheckbox"); +const fileExtensionInput = document.getElementById("fileExtensionInput"); +const attachmentFolderButton = document.getElementById('selectAttachmentFolderButton'); +const attachmentFolderSpan = document.getElementById('selectedAttachmentFolder'); + + +eventBus.addEventListener("dataReady", populateAttachmentColumnSelect); +attachmentColumnSelect.addEventListener("change", handleAttachmentColumnSelection); + + +function populateAttachmentColumnSelect(event) { + const columns = event.detail.columns; attachmentColumnSelect.innerHTML = '<option value="">Select Column</option>'; columns.forEach((column, index) => { const option = document.createElement("option"); option.value = index; - option.textContent = column || `Spalte ${index + 1}`; + option.textContent = column || `Column ${index + 1}`; attachmentColumnSelect.appendChild(option); - - // Insert the same option into the placeholder combobox: - const option2 = option.cloneNode(true); - placeholderSelect.appendChild(option2); }); attachmentColumnSelect.disabled = false; - placeholderSelect.disabled = false; } + + +appendExtensionCheckbox.addEventListener("change", () => { + fileExtensionInput.disabled = !appendExtensionCheckbox.checked; +}); +attachmentFolderButton.addEventListener("click", async () => { + try { + // TODO: This does not seem to work with the web-application of Outlook! + const directoryHandle = await window.showDirectoryPicker(); + attachmentFolderSpan.textContent = `Selected folder: ${directoryHandle.name}`; + console.log("Selected folder:", directoryHandle); + } catch (err) { + console.error("Folder selection cancelled or failed", err); + } +}); + + + function handleAttachmentColumnSelection() { if (attachmentColumnSelect.value !== "") { appendExtensionCheckbox.disabled = false; @@ -145,7 +317,22 @@ function handleAttachmentColumnSelection() { } } -function displayData(data) { + + + + +/*********************************************************** + * 4. Sanity Checks, general data handling and review + * + * + * + ***********************************************************/ + +eventBus.addEventListener("dataReady", displayData); + + +function displayData(event) { + const data = event.detail.data; const previewDiv = document.getElementById("dataPreview"); previewDiv.innerHTML = ""; @@ -168,6 +355,17 @@ function displayData(data) { previewDiv.appendChild(table); } + + + +/*********************************************************** + * 5. Mail sending + * + * + * + ***********************************************************/ + + function replacePlaceholders(template, row) { return template.replace(/{%([^%]+)%}/g, (match, columnName) => { return row[columnName] || ""; // Replace with column value or empty string @@ -183,7 +381,7 @@ async function addAttachment(mailItem, filePath, fileName) { } } -async function sendSerialMails(data, filePath) { +async function sendSerialMails(data, filePath, sendOrSave) { const item = Office.context.mailbox.item; for (const row of data) { -- GitLab