maxmilton/new-tab

View on GitHub
src/components/BookmarkBar.ts

Summary

Maintainability
A
0 mins
Test Coverage
F
49%
import { append, create, h } from 'stage1';
import { compile } from 'stage1/macro' with { type: 'macro' };
import { BookmarkNode, type BookmarkTreeNode, Folder } from './BookmarkNode';

declare global {
  interface HTMLElement {
    /**
     * BookmarkBar synthetic `mouseenter` event handler. Note the property is
     * named `__mouseover` but it actually works like `mouseenter`.
     */
    __mouseover(event: MouseEvent): void;
    /**
     * BookmarkBar synthetic `mouseleave` event handler. Note the property is
     * named `__mouseout` but it actually works like `mouseleave`.
     */
    __mouseout(event: MouseEvent): void;
  }
}

type BookmarkBarComponent = HTMLDivElement;

export const BookmarkBar = (): BookmarkBarComponent => {
  const root = create('div');
  root.id = 'b';

  void chrome.bookmarks.getChildren('1').then((bookmarks) => {
    const len = bookmarks.length;

    // Since we can't determine an element's width before it's included in the
    // DOM, we have to insert BookmarkNodes individually until no more can fit.
    // Any leftover items are then placed in an overflow folder.
    const resize = () => {
      performance.mark('BookmarkBar');

      // Remove all child nodes
      root.textContent = '';

      // Max width is root minus overflow folder width (68 == 24px svg + 2 * 9px
      // svg padding + 2 * 13px bookmark item padding)
      const maxWidth = root.clientWidth - 68;
      const otherBookmarksFolder = append(
        Folder({ id: '2', title: 'Other Bookmarks' }),
        root,
      );
      // NOTE: The elements we're measuring don't have a border or margin so
      // we can use clientWidth instead of offsetWidth for better performance.
      let width = otherBookmarksFolder.clientWidth;
      let index = 0;
      let node: ReturnType<typeof BookmarkNode>;

      for (; index < len; index++) {
        node = append(BookmarkNode(bookmarks[index]), root);
        width += node.clientWidth;

        if (width >= maxWidth) {
          // Remove the node which overflowed
          node.remove();
          break;
        }
      }

      if (index < len) {
        const overflowBookmarksFolder = append(
          Folder({} as BookmarkTreeNode, false, bookmarks.slice(index)),
          root,
        );
        overflowBookmarksFolder.className += ' end';

        append(
          h<SVGElement>(
            // https://github.com/feathericons/feather/blob/master/icons/corner-right-down.svg
            compile(`
              <svg id=io>
                <polyline points="10 15 15 20 20 15"/>
                <path d="M4 4h7a4 4 0 0 1 4 4v12"/>
              </svg>
            `).html,
          ),
          overflowBookmarksFolder,
        );
      }

      // The "Other Bookmarks" folder was added first so overflow calculation
      // is correct but now move it to its proper position at the end
      append(otherBookmarksFolder, root).className += ' end';

      performance.measure('BookmarkBar', 'BookmarkBar');
    };

    // HACK: Workaround for race condition. This script is loaded asynchronously,
    // which yields the best performance, but it means this code may execute
    // before the CSS has loaded. Styles are needed to calculate the bookmark
    // item widths, so wait until the CSS is ready.
    const waitForStylesThenResize = () => {
      // biome-ignore lint/style/useExplicitLengthCheck: byte savings
      if (document.styleSheets.length) {
        resize();
      } else {
        setTimeout(waitForStylesThenResize);
      }
    };

    waitForStylesThenResize();

    window.onresize = resize;
  });

  // Synthetic `mouseenter` and `mouseleave` event handler
  // XXX: Similar to stage1 synthetic event logic but does not stop propagating
  // once an event handler is called + extra relatedTarget checks
  // https://github.com/maxmilton/stage1/blob/08cb3c08cb3e5513c181f768ae92c488cfe2a17a/src/events.ts#L3
  // eslint-disable-next-line no-multi-assign
  root.onmouseover = root.onmouseout = (event) => {
    const eventKey = ('__' + event.type) as '__mouseover' | '__mouseout';
    // null when mouse moves from/to outside the viewport
    const related = event.relatedTarget as Node | null;
    let node = event.target as
      | (Node & {
          __mouseover?(event2: MouseEvent): void;
          __mouseout?(event2: MouseEvent): void;
        })
      | null;

    while (node) {
      if (node[eventKey] && (!related || !node.contains(related))) {
        node[eventKey](event);
      }
      node = node.parentNode;
    }
  };

  return root;
};