DIY Microsoft Office Add-Ins – Postgres WASM in the Task Pane

Poking around the org’s Office 365 offering today, I noticed in Word the ability to add “Add-ins”, sidebar extensions essentially.

There’s lots of them in the list, but many of them require some sort of enabling from OUr 364 admins, whoch rather begs the question why Microsoft should be allowed to pump such stuff into our corporate Office environment, but I guess that’s another thing that just being a customer of behemoth means you haver to suck up and accept.

Anyway… it looks like you can upload your own… (whether they’d word without approval I don’t know…)

Anyway, new to me, so how can it be to write one?

There’s set-up docs and tutorials and API docs, and at its simplest for a developer preview you “just” need to install some node stuff for the build and then tinker with some HTML, js, and css.

# On the command line
npm install -g yo generator-office
yo office

# I also found this useful to kill node webwerver
# npx kill-port 3000

I selected the Office Add-in Task Pane project option, then Javascript then Word.

As with modt mode things, this downloads the internet. For each new extension you build.

Unlike the battles I’ve had trying to build things for JupyterLab, it dodnlt take much time to repurpose my pglite (minimal postgres) demo that runs a WASM based version of Postgres in the browser to something that runs it in the Word task pane:

The demo runs via a local server in the Word app on my desktop.

In the above example, a “for example” use case might be:

  • I’m writing some SQL teaching materials;
  • I knock up some SQL in the Word doc;
  • I select it, and click a button in the task pane that copies the selected text over to the task pane;
  • I click another button that runs the SQL against the postgres WASM app running in the task pane, and it displays the result;
  • if the query doesn’t work as intended, I can fettle it in the task pane until it does work;
  • if necessary, I click a button to replace the original (broken) SQL in the Word doc with the corrected SQL;
  • if required, I click a button and it grabs my SQL result table and pastes it as a Word table at the end of the doc (hmm, maybe this should be to the cursor location?)

Here’s the HTML for the task pane view:

<pre class="wp-block-syntaxhighlighter-code"><!-- Originally Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>OUseful-pglite Task Pane Add-in</title>

    <!-- Office JavaScript API -->
    <a href="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js">https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js</a>

    <!-- Bring in a OUseful file-->
    <a href="http://pglite.js">http://pglite.js</a>

    <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css"/>

    <!-- Template styles -->
    <link href="taskpane.css" rel="stylesheet" type="text/css" />
</head>

<body class="ms-font-m ms-welcome ms-Fabric">
    <header class="ms-welcome__header ms-bgColor-neutralLighter">
        <h1 class="ms-font-su">ouseful.info WASM demo</h1>
    </header>
    <section id="sideload-msg" class="ms-welcome__main">
        <h2 class="ms-font-xl">Please <a target="_blank" href="https://learn.microsoft.com/office/dev/add-ins/testing/test-debug-office-add-ins#sideload-an-office-add-in-for-testing">sideload</a> your add-in to see app body.</h2>
    </section>
    <main id="app-body" class="ms-welcome__main" style="display: none;">
        <h2 class="ms-font-xl">WASM app running in sidebar...</h2>
        <div>Simple pglite application running in the browser. Example query: <tt>select * from test;</tt><br/><br/></div>
        <textarea id="query" rows="4" cols="30"></textarea>
    <div><button id="getsel">Get Selection</button><button id="execute">Execute</button><button id="exepaste">Paste Result</button><button id="replacesel">Replace selection</button></div>
    <div id="timestamp"></div>
    <div id="output"></div>
    <div id="results"></div>
    </main>
</body>

</html></pre>

And the JS from my pglite demo:

// PGLite loader
import { PGlite } from "https://cdn.jsdelivr.net/npm/@electric-sql/pglite/dist/index.js";

// Initialize PGlite
const db = new PGlite();
// We can persist the db in the browser
//const db = new PGlite('idb://my-pgdata')

const DEFAULT_SQL = `
-- Optionally select statements to execute.

CREATE TABLE IF NOT EXISTS test  (
        id serial primary key,
        title varchar not null
      );

INSERT INTO test (title) values ('dummy');

`.trim();

async function createTable() {
  await db.query(DEFAULT_SQL);
  await db.query("select * from test").then((resultsX) => {
    console.log(JSON.stringify(resultsX));
  });
}
createTable();
window.addEventListener("DOMContentLoaded", async function () {
  const button = /** @type {HTMLButtonElement} */ (
    document.getElementById("execute")
  );

  // Execute SQL on button click.
  button.addEventListener("click", async function () {
    button.disabled = true;

    // Get SQL from editor.
    const queries = document.getElementById("query").value;

    // Clear any previous output on the page.
    const output = document.getElementById("output");
    while (output.firstChild) output.removeChild(output.lastChild);

    const timestamp = document.getElementById("timestamp");
    timestamp.textContent = new Date().toLocaleTimeString();

    let time = Date.now();
    console.log(`${queries}`);
    try {
      const results = await db.query(`${queries}`);
      //.then(results => {console.log("results are"+JSON.stringify(results))});

      const resultsDiv = document.getElementById("results");
      resultsDiv.innerHTML = "";
      const table = formatTable(results);
      formatRows(results, table);
      resultsDiv.appendChild(table);
    } catch (e) {
      // Adjust for browser differences in Error.stack().
      const report = (window["chrome"] ? "" : `${e.message}\n`) + e.stack;
      output.innerHTML = `<pre>${report}</pre>`;
    } finally {
      timestamp.textContent += ` ${(Date.now() - time) / 1000} seconds`;
      button.disabled = false;
    }
  });
});

function formatTable(results) {
  const table = document.createElement("table");

  const headerRow = table.insertRow();
  Object.keys(results[0]).forEach((key) => {
    const th = document.createElement("th");
    th.textContent = key;
    headerRow.appendChild(th);
  });
  return table;
}

function formatRows(results, table) {
  results.forEach((rowData) => {
    const row = table.insertRow();
    Object.values(rowData).forEach((value) => {
      const cell = row.insertCell();
      cell.textContent = value;
    });
  });
}

And the very task pane js:

/*
 * Originally Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
 * See LICENSE in the project root for license information.
 */

/* global document, Office, Word */

Office.onReady((info) => {
  if (info.host === Office.HostType.Word) {
    document.getElementById("sideload-msg").style.display = "none";
    document.getElementById("app-body").style.display = "flex";
    document.getElementById("exepaste").onclick = exePaste;
    document.getElementById("getsel").onclick = getSel;
    document.getElementById("replacesel").onclick = replaceSel;
  }
});


async function getSel() {
  await Word.run(async (context) => {
    // Get SQL from selection
    const selected = context.document.getSelection();
    selected.load("text");
    await context.sync();
    document.getElementById("query").value = selected.text;
    //const paragraph = context.document.body.insertParagraph("Running command: " + selected.text, Word.InsertLocation.end);
    await context.sync();

  });
}

async function replaceSel() {
  await Word.run(async (context) => {
    // Get SQL from selection
    const selected = context.document.getSelection();
    const replace_text = document.getElementById("query").value;
    selected.insertText(replace_text, Word.InsertLocation.replace);
    //const paragraph = context.document.body.insertParagraph("Running command: " + selected.text, Word.InsertLocation.end);
    await context.sync();
  });
}

async function exePaste() {
  await Word.run(async (context) => {
    // Get SQL from editor.
    //const txt = document.getElementById("results").innerHTML;
    //const paragraph = context.document.body.insertParagraph(txt, Word.InsertLocation.end);
    //var parser = new DOMParser();
    //var htmlDoc = parser.parseFromString(txt, "text/html");
    //var table = htmlDoc.getElementsByTagName("table")[0];
    var table = document.getElementById("results").firstChild;
    var rows = table.rows;
    var numCols = rows[0].cells.length;
    var numRows = rows.length;
    // Extract table data into a two-dimensional array
    var tableData = [];
    for (var i = 0; i < numRows; i++) {
      var rowData = [];
      for (var j = 0; j < numCols; j++) {
        rowData.push(rows[i].cells[j].innerText);
      }
      tableData.push(rowData);
    }

    // Insert the table into the Word document
    const wtable = context.document.body.insertTable(numRows, numCols, Word.InsertLocation.end, tableData);

    await context.sync();

  });
}

For completeness, here’s the css:

/* 
 * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
 * See LICENSE in the project root for license information.
 */

 html,
 body {
     width: 100%;
     height: 100%;
     margin: 0;
     padding: 0;
 }
 
 ul {
     margin: 0;
     padding: 0;
 }
 
 .ms-welcome__header {
    padding: 20px;
    padding-bottom: 30px;
    padding-top: 100px;
    display: -webkit-flex;
    display: flex;
    -webkit-flex-direction: column;
    flex-direction: column;
    align-items: center;
 }

 .ms-welcome__main {
    display: -webkit-flex;
    display: flex;
    -webkit-flex-direction: column;
    flex-direction: column;
    -webkit-flex-wrap: nowrap;
    flex-wrap: nowrap;
    -webkit-align-items: center;
    align-items: center;
    -webkit-flex: 1 0 0;
    flex: 1 0 0;
    padding: 10px 20px;
 }
 
 .ms-welcome__main > h2 {
     width: 100%;
     text-align: center;
 }
 
 .ms-welcome__features {
     list-style-type: none;
     margin-top: 20px;
 }
 
 .ms-welcome__features.ms-List .ms-ListItem {
     padding-bottom: 20px;
     display: -webkit-flex;
     display: flex;
 }
 
 .ms-welcome__features.ms-List .ms-ListItem > .ms-Icon {
     margin-right: 10px;
 }
 
 .ms-welcome__action.ms-Button--hero {
     margin-top: 30px;
 }
 
.ms-Button.ms-Button--hero .ms-Button-label {
  color: #0078d7;
}

.ms-Button.ms-Button--hero:hover .ms-Button-label,
.ms-Button.ms-Button--hero:focus .ms-Button-label{
  color: #005a9e;
  cursor: pointer;
}

b {
    font-weight: bold;
}

      #editor-container {
        width: 100%;
        height: 20vh;
        border:lightgray;
        border-style: solid;
      }

      #vfs-container {
        margin-top: 0.5em;
        margin-bottom: 0.5em;
      }
      #timestamp {
        margin-top: 0.5em;
      }

      #output {
        display: flex;
        flex-wrap: wrap;
        width: 100%;
      }

      table {
        margin-top: 1em;
        margin-left: 0.4em;
        margin-right: 0.4em;
        border-collapse: collapse;
      }

      td, th {
        border: 1px solid #999;
        padding: 0.5rem;
        text-align: left;
      }

Next up, I think I’lll have a look at adding a simple pyodide / Python sandbox into the task pane, perhaps trying the “react” rather than javascript cookiecutter (I think that offers a different way of loading js and wasm assets?)

This would then allow a similar pattern of “author code in the Word doc, try it out, fix it, paste the fix back, paste the result back”. It’s nto a reperoducible pattern, but it does shorten the loop between code and output, and also means you can test the code (sort of) whilst authoring.

PS Hmm, I wonder, could I style the code in the word doc then extract just all the styled code, or “all above” styled code, and run it in the sidebar? One to try tomorrow, maybe…

Author: Tony Hirst

I'm a Senior Lecturer at The Open University, with an interest in #opendata policy and practice, as well as general web tinkering...

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.