hugo-coder icon indicating copy to clipboard operation
hugo-coder copied to clipboard

[v2] Add Table of Content

Open luizdepra opened this issue 3 years ago • 10 comments

Add TOC support.

I need to decide with style to use first. (I didn't like the aside style. I'll probably go with the on top style.) See discussion in #329.

luizdepra avatar Sep 28 '20 17:09 luizdepra

I was looking at docsy to see how they approached it. They have it like a side bar.

image

The responsive view for it is pretty nice too. (Happens when the navigation button is selected)

image

Their approach seems to be like "side bar" normally but changes to be "on top" when the screen becomes smaller. Seems to be like one of the approaches that can be used.

That being said also, if it is a side bar, when users navigate down on the main contents, they would be navigating away from the side bar. (Depending on the amount of contents in the sidebar table of contents). So should the it be in a fixed position at the side? Meaning when the user navigates the main contents, the sidebar position stays the same?

With regards to the top style, it seems that it might be too many contents there causing it to occupy too much space.

What do you think of their approach?

JianLoong avatar Oct 09 '20 12:10 JianLoong

I think a side like docsy have doesn't fit well with coder's style. LoveIt, that someone mentioned in #329, looks like a better option. It also stays on top in smaller screens.

But I still have mixed feelings about this issue. I really don't know yet which of these approaches is the better one.

OFFTOPIC: I just discovered that LoveIt is based on other themes that are based on coder, and this is very nice. :)

luizdepra avatar Oct 13 '20 18:10 luizdepra

Is it possible to render any table of contents at all (even if not styled yet) with this theme? I added {{ .TableOfContents }} to ./themes/hugo-coder/layouts/posts but the variable {{ .TableOfContents }} seems to be empty. It does not error when rendering, so the variable seems to exist, even though it is not set to anything.

Any help on how to get a simple TOC?

pinpox avatar Jan 13 '21 12:01 pinpox

@pinpox, It should go here before {{.Content}}.

luizdepra avatar Jan 22 '21 19:01 luizdepra

@luizdepra This is my themes/hugo-coder/layouts/posts/single.html file:

{{ define "title" }}
  {{ .Title }} · {{ .Site.Title }}
{{ end }}
{{ define "content" }}
  <section class="container post">
    <article>
      <header>
        <div class="post-title">
          <h1 class="title">{{ .Title }}</h1>
        </div>
        <div class="post-meta">
          <div class="date">
            <span class="posted-on">
              <i class="fa fa-calendar" aria-hidden="true"></i>
              <time datetime='{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}'>
                {{ .Date.Format (.Site.Params.dateFormat | default "January 2, 2006" ) }}
              </time>
            </span>
            <span class="reading-time">
              <i class="fa fa-clock-o" aria-hidden="true"></i>
              {{ i18n "reading_time" .ReadingTime }}
            </span>
          </div>
          {{ with .Page.Params.Authors }}{{ partial "taxonomy/authors.html" . }}{{ end }}
          {{ with .Page.Params.Categories }}{{ partial "taxonomy/categories.html" . }}{{ end }}
          {{ with .Page.Params.Tags }}{{ partial "taxonomy/tags.html" . }}{{ end }}
        </div>
      </header>

      <div>
        {{ if .Params.featured_image }}
          <img src='{{ .Params.featured_image }}' alt="Featured image"/>
        {{ end }}
		{{ .TableOfContents }}
        {{ .Content }}
      </div>


      <footer>
        {{ partial "posts/series.html" . }}
        {{ partial "posts/disqus.html" . }}
        {{ partial "posts/commento.html" . }}
        {{ partial "posts/utterances.html" . }}
      </footer>
    </article>

    {{ partial "posts/math.html" . }}
  </section>
{{ end }}

But I dont' see any a ToC in my posts when running hugo serve. Do I have to add it in the posts's markdown too?

pinpox avatar Jan 23 '21 17:01 pinpox

@pinpox, nop. No need to change your markdowns.

I just did the same thing and tested with exampleSite content.

Screenshot from 2021-01-25 16-15-02

Are you certain that you are testing against a post with correct headings? By default, the TOC you not show if no displayable headings are present. Also, level 1 headings will not show unless configured to do so. See TOC configurations here.

luizdepra avatar Jan 25 '21 19:01 luizdepra

If you are using VScode you can create this type of TOC using "markdown all in one" plugin. And on top TOC goes out of sight when you scroll down. So I was wondering if something like this TOC and go-to-up arrow would be good choice?

prishs avatar Mar 04 '21 06:03 prishs

I added a table of contents to my blog。 if you think this style is ok, I would be happy to create a pull requset

https://chenkai.life/web/handling-fonts-in-the-web

  • Widescreen 截屏2021-09-27 下午3 34 50 截屏2021-09-27 下午3 32 30

  • phone screen

截屏2021-09-27 下午3 32 44 截屏2021-09-27 下午3 34 41

ckvv avatar Sep 27 '21 10:09 ckvv

Hi. I want to know how is the progress of this feature? Thanks a lot for your development!

Oyyko avatar Jul 31 '23 19:07 Oyyko

Hi. I want to know how is the progress of this feature? Thanks a lot for your development!

截屏2023-08-01 13 59 31

you can try import this script

function debounce(func, wait, options = {
  immediate: false,
  middle: true,
  thisArg: null,
}) {
  let timer;
  let restDate = new Date();
  const immediate = options.immediate !== false;
  const middle = options.middle !== false;
  const thisArg = options.thisArg || null;
  return function (...args) {
    timer && clearTimeout(timer);
    let isFirst = !timer;
    timer = setTimeout(() => {
      func.apply(thisArg, args);
      restDate = new Date();
    }, wait);
    if ((new Date() - restDate > wait && middle) || (isFirst && immediate)) {
      clearTimeout(timer);
      func.apply(thisArg, args);
      restDate = new Date();
    }
  }
}

function setActive(anchors) {
  const ele = anchors.find((ele, index, arr) => {
    return ele.getBoundingClientRect().top >= 0 || index >= arr.length - 1;
  });
  if (ele) {
    const tableOfContents = document.querySelector('#table-of-contents');
    const toActive = tableOfContents.querySelector(`a[href="#${ele.id}"]`);
    if (!toActive) return;
    const activeA = tableOfContents.querySelector(`.active`);
    if (activeA) activeA.classList.remove('active');
    toActive.classList.add('active');
    window.history.pushState(null, null, `#${ele.id}`);
    tableOfContents.scrollTo({
      left: 0,
      top: toActive.offsetTop - tableOfContents.getBoundingClientRect().height / 2,
      behavior: 'smooth',
    });
  }
}

function initContents(icon = '<svg t="1690868184633" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3381" width="32" height="32"><path d="M128 192l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3382"></path><path d="M128 448l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3383"></path><path d="M128 704l768 0 0 128-768 0 0-128Z" fill="#666666" p-id="3384"></path></svg>') {
  if(document.querySelector('#table-of-contents-wapper')) return;
  const contents = document.createElement('details');
  contents.id = 'table-of-contents-wapper';
  contents.innerHTML = `<summary>
    ${icon}
  </summary>`
  const styleElement = document.createElement('style')
  styleElement.innerHTML = `#table-of-contents-wapper {
    user-select: none;
    position: fixed;
    right: 1em;
    top: 4em;
    border-radius: 8px;
    z-index: 999
  }

  @media only screen and (min-width:768px) {
    #table-of-contents-wapper[open] summary {
      position: relative;
      left: 5.8em
    }
  }

  @media only screen and (max-width:768px) {
    #table-of-contents-wapper {
      top: auto;
      right: auto;
      bottom: 1.8rem;
      left: 1.8rem
    }
  }

  #table-of-contents-wapper summary {
    display: inline-block;
    font-size: 1.5em;
    border-radius: 4px;
    cursor: pointer;
    padding: .2em
  }

  #table-of-contents-wapper #table-of-contents {
    line-height: 1.3;
    width: 19rem;
    font-size: .8rem;
    padding: .8em;
    border: 1px solid;
    border-radius: 6px;
    overflow-y: scroll;
    max-height: calc(100vh - 20rem);
    color: currentColor
  }

  #table-of-contents-wapper #table-of-contents a {
    width: 100%;
    display: inline-block;
    color: currentColor;
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis
  }

  #table-of-contents-wapper #table-of-contents .active {
    border-left: 2px solid #42a5f5;
    color: #42a5f5
  }

  .content h1,
  .content h2,
  .content h3 {
    padding-top: 2em !important;
    margin-top: -2em !important
  }`;

  const anchors = [...document.querySelector('.content').querySelectorAll('h1[id],h2[id],h3[id],h4[id]')];
  if (!anchors.length) return contents.remove();
  const tableOfContents = document.createElement('div');
  tableOfContents.id = 'table-of-contents';
  anchors.forEach(ele => {
    const a = document.createElement('a');
    if (!ele.innerText) return;
    a.innerText = ele.innerText;
    a.href = `#${ele.id}`;
    a.style.paddingLeft = `${ele.tagName.charAt(1)}em`;
    tableOfContents.appendChild(a);
  });

  contents.appendChild(tableOfContents);
  contents.open = window.innerWidth >= 768;

  document.head.appendChild(styleElement);
  document.body.append(contents);

  setActive(anchors);
  const debounceSetActive = debounce(setActive, 200)
  window.addEventListener('scroll', () => {
    debounceSetActive(anchors);
  });
}

and run the initContents function

ckvv avatar Aug 01 '23 06:08 ckvv