How to insert content at the end of a specific section in Word Add-in using Office.js?

Huzefa Khan 0 Reputation points
2025-05-21T04:36:26.9933333+00:00

Question:

I'm developing a Word Add-in using Office.js and inserting reference links under a "References" heading.

Issue: Each new reference is being inserted at the top of the "References" section, causing the order to be reversed:

5. Latest inserted
4. Previous
3. ...

Expected Behavior: I want new references to be appended at the bottom of the "References" section so the list appears as:

1. First
2. Second
3. ...

What I’ve tried: I'm traversing the document to find the "References" heading and inserting the new reference after the last non-empty paragraph in that section. However, the new reference still appears at the top.

Question: How can I ensure the content is inserted after the last reference paragraph under a specific heading like "References" using Office.js?

Current complete function code - JS


const insertReference = (editor, title, url, skipInlineTag = false) => {
  if (!url || !title) {
    alert('Both Title and URL are required!');
    return;
  }

  const model = editor.model;

  model.change(writer => {
    const root = model.document.getRoot();
    let existingNumbers = new Set();

    let referencesStarted = false;
    let otherResourcesStarted = false;
    let referenceSectionStart = null;
    let otherResourcesSectionStart = null;
    let insertAfterElement = null;

    for (const child of root.getChildren()) {
      if (child.is('element', 'paragraph')) {
        const paraText = Array.from(child.getChildren())
          .filter(node => node.is('text'))
          .map(node => node.data)
          .join('')
          .trim();
    
        // Check if it's a heading
        if (/^references$/i.test(paraText)) {
          referencesStarted = true;
          otherResourcesStarted = false;
          referenceSectionStart = child;
          continue;
        } else if (/^other resources$/i.test(paraText)) {
          otherResourcesStarted = true;
          referencesStarted = false;
          otherResourcesSectionStart = child;
          continue;
        } else if (/^[A-Z][a-z]+( [A-Z][a-z]+)*$/.test(paraText)) {
          // This regex detects other section titles like "Introduction", "Appendix" etc.
          referencesStarted = false;
          otherResourcesStarted = false;
        }
    
        if (referencesStarted && !skipInlineTag) {
          const numberedMatches = [...paraText.matchAll(/^(\d+)\.\s/g)];
          numberedMatches.forEach(m => existingNumbers.add(parseInt(m[1])));
        }
    
        // Identify where to insert
        if ((referencesStarted && !skipInlineTag) || (otherResourcesStarted && skipInlineTag)) {
          const paraText = Array.from(child.getChildren())
            .filter(node => node.is('text'))
            .map(node => node.data)
            .join('')
            .replace(/\u00A0/g, ' ') // Convert   to space
            .trim();
        
          // Only update insertAfterElement if it's not empty/whitespace-only
          if (paraText.length > 0) {
            insertAfterElement = child;
          }
        }
      }
    }
    

    const nextRef = existingNumbers.size ? Math.max(...existingNumbers) + 1 : 1;
    const refId = `ref-${nextRef}`;

    // Insert inline [n] tag
    if (!skipInlineTag) {
      const refText = writer.createText(`[${nextRef}]`, {
        linkHref: `#${refId}`
      });
      editor.model.insertContent(refText);
    }

    const targetSection = skipInlineTag ? "Other Resources" : "References";

    // Insert section heading if missing
    const hasSection = [...root.getChildren()].some(child => {
      if (child.is('element', 'paragraph')) {
        const text = Array.from(child.getChildren())
          .filter(n => n.is('text'))
          .map(n => n.data.toLowerCase().trim())
          .join('');
        return text === targetSection.toLowerCase();
      }
      return false;
    });

    if (!hasSection) {
      const headerHtml = `<p style="font-weight:bold">${targetSection}</p>`;
      const viewHeader = editor.data.processor.toView(headerHtml);
      const modelHeader = editor.data.toModel(viewHeader);
      writer.insert(modelHeader, root, 'end');
    }

    // Create the reference HTML block
    const referenceHtml = skipInlineTag
      ? `<p><a href="${url}" target="_blank"><span style="font-size:12px;"><u>${title}</u></span></a></p>`
      : `<p><a name="${refId}"></a><span style="font-size:12px;">${nextRef}. </span><a href="${url}" target="_blank"><span style="color:#0000ff;"><span style="font-size:12px;"><u>${title}</u></span></span></a></p>`;

    const viewFragment = editor.data.processor.toView(referenceHtml);
    const modelFragment = editor.data.toModel(viewFragment);

    // Insert at proper position
    if (insertAfterElement) {
      writer.insert(modelFragment, insertAfterElement, 'after');
    } else {
      // Fallback to inserting at end if section not found
      writer.insert(modelFragment, root, 'end');
    }
  });
};
JavaScript API
JavaScript API
An Office service that supports add-ins to interact with objects in Office client applications.
1,062 questions
0 comments No comments
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.