Developer Guide to Document Authoring with Edge Delivery Services. Part 7

Author: Tom Cranstoun

Useful Components

Blocks I use in my guides.

  • Index
  • Returntotop
  • Bloglist

I explain the additional blocks I use in my guides.

Previously

We discussed Google Sheets/ Excel spreadsheets, Edge Delivery Services automatic conversion to Json, and the ability to create fallbacks in Github.

Part 7 - Useful Components

In building this series, I have created three blocks (or components, in old CMS speak) that help provide navigation widgets.

Each block is designed to be intuitive for the user and easy for the content author, automating the work. The content author just has to place the item in the correct place in the document, as a block fragment, queued in by a table. The code achieves this functionality using vanilla JavaScript, CSS 3 and DOM manipulation; no frameworks or libraries are involved - this is the perfect way to create Edge Delivery Services blocks (components).

returntotop

I will begin with the simplest one, returntotop

The function makes the "Back to Top" button visible when the user scrolls down over 100 pixels. When the button is clicked, the page smoothly scrolls back to the top.

This block has two cells: the first one is the block name, and the second is the content. The content is the wording the author wants to use when it is displayed; one may wish to use different phrasing or say it in French; it is up to the content author.

As always, create the folder returntotop under the blocks folder, then make two files returntotop.css and returntotop.js

returntotop.css contains the styling.

.returntotop {
    position: fixed;
    bottom: 20px;
    left: 20px;
    padding: 10px 20px;
    background-color: #007BFF;
    color: white;
    cursor: pointer;
    text-align: center;
    border-radius: 5px;
    text-decoration: none;
    font-size: 14px;
    display: none;
}

.returntotop:hover {
    background-color: #0056b3;
}

.

The returntotop.js contains the code.

export default async function decorate(block) {
  const returnToTopButton = document.querySelector('.returntotop');
  window.addEventListener('scroll', () => {
    if (window.scrollY > 100) {
      returnToTopButton.style.display = 'block';
    } else {
      returnToTopButton.style.display = 'none';
    }
  });

  // Scroll to top when the button is clicked
  returnToTopButton.addEventListener('click', () => {
    window.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  });
}

It adds an event listener to the window object that listens for the scroll event. The button is displayed when the user scrolls down more than 100 pixels (display: 'block'). Otherwise, the button is hidden (display: 'none').

index

Once again, create a folder inside blocks called index and two files, index.css and index.js, in this folder.

Index.css should contain

.index {
    background-color: #f5f5f5;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 10px;
    cursor: pointer;
}

.index-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.arrow {
    border: solid black;
    border-width: 0 2px 2px 0;
    display: inline-block;
    padding: 3px;
    transition: transform 0.3s;
}

.down {
    -webkit-transform: rotate(45deg);
    transform: rotate(45deg);
}

.index-content {
    display: none;
    margin-top: 10px;
}

.index-content ul {
    list-style-type: none;
    padding: 0;
}

.index-content ul li {
    margin-bottom: 5px;
}

.index-content ul li a {
    text-decoration: none;
    color: #333;
}

And index.js

export default function decorate(block) {
  const headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
  const indexBlock = document.querySelector('.index');
  // Create the index header
  const indexHeader = document.createElement('div');
  indexHeader.className = 'index-header';
  indexHeader.innerHTML = `
    <span>Index</span>
    <i class='arrow down'></i>
  `;
  // Create the index content container
  const indexContent = document.createElement('div');
  indexContent.className = 'index-content';
  // Append the index header and content container to the index block
  indexBlock.appendChild(indexHeader);
  indexBlock.appendChild(indexContent);

  let isIndexBuilt = false; // Flag to track if the index has been built

  indexHeader.addEventListener('click', () => {
    if (!isIndexBuilt) {
      buildIndex();
      isIndexBuilt = true; // Set the flag to true after building the index
      indexContent.style.display = 'none';
    }

    if (indexContent.style.display === 'none') {
      indexContent.style.display = 'block';
      indexHeader.querySelector('.arrow').style.transform = 'rotate(-135deg)';
    } else {
      indexContent.style.display = 'none';
      indexHeader.querySelector('.arrow').style.transform = 'rotate(45deg)';
    }
  });

  function buildIndex() {
    const indexContent2 = document.querySelector('.index-content');
    const ul = document.createElement('ul');
    headers.forEach((header, index) => {
      const id = `header-${index}`;
      header.id = id;
      const li = document.createElement('li');
      li.style.marginLeft = `${(parseInt(header.tagName[1], 10) - 1) * 20}px`;
      const a = document.createElement('a');
      a.href = `#${id}`;
      a.textContent = header.textContent;
      li.appendChild(a);
      ul.appendChild(li);
    });
    indexContent2.innerHTML = '';
    indexContent2.appendChild(ul);
  }
}

This code creates an interactive webpage index by selecting all header elements (h1 to h6) and the index block. It creates an index header with a clickable arrow and an index content container. The index is built lazily - only when the user first clicks on the index header. Clicking the index header toggles the visibility of the index content and rotates the arrow.

The buildIndex function creates a list of links to all headers on the page, indenting them based on their header level (h1, h2, etc.). Each header is given a unique ID, and the index links are created to point to these IDs.

Finally

bloglist

Create a folder inside blocks called bloglist and then add the two files bloglist.css and bloglist.js.

Bloglist.css

.bloglist {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 20px;
}

.blog-item {
    border: 1px solid #ccc;
    padding: 20px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
}

.blog-item img {
    width: 100%;
    height: 280px;
    object-fit: cover;
    margin-bottom: 10px;
}

.blog-item p {
    margin: 10px 0;
}

.blog-item .last-modified {
    margin-top: auto;
    padding-top: 10px;
    position: relative;
}

.blog-item .last-modified::before {
    content: '';
    position: absolute;
    top: 0;
    left: -20px;
    right: -20px;
    border-top: 1px solid #ccc;
}

@media screen and (max-width: 600px) {
    .bloglist {
        grid-template-columns: 1fr;
    }
}

This CSS creates a responsive blog list layout, using CSS Grid for the blog list, with auto-fitting columns. Each blog item is a flex container with a border. Blog images are set to a fixed height with full width. The last modified date is pushed to the bottom with a separator line. On mobile (< 600px), it switches to a single-column layout.

The Javascript file

export default async function decorate(block) {
  const blogListElement = document.querySelector(".bloglist");
  const url = "/query-index.json";
  const currentPath = window.location.pathname; // Get the current document's path

  try {
    const response = await fetch(url);
    const data = await response.json();

    // Filter the blog items based on the presence of "/blogs/ddt/a-developer" in the path
    // and exclude the current document
    const filteredBlogItems = data.data.filter((item) =>
      item.path.includes("developer-guide") && item.path !== currentPath
    );

    // Sort the filtered blog items by title
    const sortedBlogItems = filteredBlogItems.sort((a, b) =>
      a.title.localeCompare(b.title)
    );

    
    const limitedBlogItems = sortedBlogItems.slice(0, 4);

    // generate the content
    const content = generateContent(sortedBlogItems);

    blogListElement.innerHTML = content;
  } catch (error) {
    console.error("Error fetching the JSON data:", error);
  }
}
function generateContent(blogItems) {
  let content = "";

  blogItems.forEach((item) => {
    const lastModifiedDate = new Date(item.lastModified * 1000);
    const formattedDate = formatDate(lastModifiedDate);

    content += `
            <div class="blog-item">
                <a href="${item.path}">      
                    <img src="${item.image}" alt="${item.title}">
                    <strong>${item.title}</strong>
                </a>
                <p>${item.description}</p>
                <p class="last-modified">Last Modified: ${formattedDate}</p>
            </div>
        `;
  });

  return content;
}

function formatDate(date) {
  const day = String(date.getDate()).padStart(2, "0");
  const month = getMonthName(date.getMonth());
  const year = date.getFullYear();

  return `${day}/${month}/${year}`;
}

function getMonthName(monthIndex) {
  const monthNames = [
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December",
  ];

  return monthNames[monthIndex];
}

Javascript fetches blog data from "/query-index.json," filters blog items to include only those with "developer-guide" in the path, sorts the filtered items alphabetically by title, generates HTML content for the blog list, and inserts the generated content into the DOM.

code-expander

The Code Expander block adds a copy-to-clipboard functionality to all <code> elements in the document.

Usage

This block automatically enhances all <code> elements on the page, so no specific usage in the content is required.

Authoring

Authors can continue to use the standard <code> element in their content by applying the courier font to a block of β€˜code’.

The expander is agitated by adding a simple block to the page.

The Code Expander block will automatically add the copy-to-clipboard functionality, an icon and a copy-to-clipboard text.

Like this

Styling

The block uses the following CSS classes:

The <pre> element within .code-expander-code has a light background for visual distinction.

Behavior

Accessibility

code-expander.js

export default async function decorate(block) {
  const codeElements = document.querySelectorAll('code');

  codeElements.forEach((codeElement) => {
    const wrapper = document.createElement('div');
    wrapper.className = 'code-expander-wrapper';

    const copyButton = document.createElement('button');
    copyButton.className = 'code-expander-copy';
    copyButton.innerHTML = 'πŸ“‹ <span class="code-expander-copy-text">Copy code to clipboard</span>';
    copyButton.setAttribute('aria-label', 'Copy code to clipboard');
    copyButton.title = 'Copy to clipboard';

    const codeWrapper = document.createElement('div');
    codeWrapper.className = 'code-expander-code';
    codeWrapper.appendChild(codeElement.cloneNode(true));

    wrapper.appendChild(copyButton);
    wrapper.appendChild(codeWrapper);

    codeElement.parentNode.replaceChild(wrapper, codeElement);

    copyButton.addEventListener('click', async () => {
      try {
        await navigator.clipboard.writeText(codeWrapper.textContent);
        copyButton.innerHTML = 'βœ… <span class="code-expander-copy-text">Copied!</span>';
        copyButton.setAttribute('aria-label', 'Code copied to clipboard');
        setTimeout(() => {
          copyButton.innerHTML = 'πŸ“‹ <span class="code-expander-copy-text">Copy code</span>';
          copyButton.setAttribute('aria-label', 'Copy code to clipboard');
        }, 2000);
      } catch (err) {
        // eslint-disable-next-line no-console
        console.error('Failed to copy text: ', err);
      }
    });
  });
}

Code-expander.css

.code-expander-wrapper {
  position: relative;
  display: block;
  padding: 30px 10px 10px;
}

.code-expander-copy {
  position: absolute;
  top: -12px;
  left: 10px;
  background: #fff;
  border: 1px solid #ccc;
  font-size: 0.9em;
  cursor: pointer;
  opacity: 0.9;
  transition: opacity 0.3s ease;
  display: flex;
  align-items: center;
  padding: 2px 5px;
  border-radius: 3px;
}

.code-expander-copy:hover {
  opacity: 1;
}

.code-expander-copy-text {
  font-size: 0.8em;
  margin-left: 5px;
  color: #000;
}

.code-expander-code {
  margin-top: 5px;
}

.code-expander-code pre {
  padding: 10px;
  background-color: #f8f8f8;
}

@media (max-width: 768px) {
  .code-expander-copy {
    font-size: 0.8em;
  }
  
  .code-expander-copy-text {
    font-size: 0.7em;
  }
}
pre {
    border: 2px solid #0a0909;
    border-radius: 4px;
  }
/fragments/ddt/proposition

Related Articles

guide
Back to Top