2

Imagine a html text that contains headlines (h1-h6). In my situtation it is also present as DOM in a html page. So using jQuery I would do something like $('.text').find('h1,h2,h3,h4,h5,h6') to extract all the headlines.

But I don't want to use jQuery or any other heavy framework. How can I do this with angularJS? Please remember that I need the headlines in the correct order to display it as a table of contents.

5 Answers 5

3

So here is my final solution. The ng-model part is used to update the headlines when the text is updated.

.directive('tableOfContents', function(){
    return {
        restrict:'A',
        require:'?ngModel',
        link : function(scope, elm, attrs,ngModel) {
            function updateHeadlines() {
                scope.headlines=[];
                angular.forEach(elm[0].querySelectorAll('h1,h2,h3,h4,h5,h6'), function(e){
                    scope.headlines.push({ 
                        level: e.tagName[1], 
                        label: angular.element(e).text(),
                        element: e
                    });
                });
            }
            // avoid memoryleaks from dom references
            scope.$on('$destroy',function(){
                scope.headlines=[];
            });
            // scroll to one of the headlines
            scope.scrollTo=function(headline){
                headline.element.scrollIntoView();
            }
            // when the html updates whe update the headlines
            ngModel.$render = updateHeadlines;
            updateHeadlines();
        }
    }
})

Usage:

<a ng-repeat="headline in headlines" ng-click="scrollTo(headline)">{{headline.label}}</a>
<div table-of-contents ng-model="html">{{html}}</div>
Sign up to request clarification or add additional context in comments.

Comments

1

Take a look at this library - https://www.cssscript.com/generating-a-table-of-contents-with-pure-javascript-toc/

You can simply place the js code in your controller and it will place all the headers under a tag with id #toc

in your html file create

<div class="col-md-3 col-xs-12">
    <aside id="toc"></aside> <!-- Content will appear here -->
</div>
<div class="col-md-9 col-xs-12 no-padding">
    <div id="doc-content">
    ... Your content is here ...
</div>

And then in your controller include this function. You can call it from onInit for example.

loadTOC(){


    // Definitions
    var extendObj = function (src, target) {
      for (var prop in target) {
          if (target.hasOwnProperty(prop) && target[prop]) {
              src[prop] = target[prop];
          }
      }

      return src;
    };

    var getHeaders = function (selector, scope) {
        var ret = [];
        var target = document.querySelectorAll(scope);

        Array.prototype.forEach.call(target, function (elem) {
            var elems = elem.querySelectorAll(selector);
            ret = ret.concat(Array.prototype.slice.call(elems));
        });

        return ret;
    };

    var getLevel = function (header) {
        if (typeof header !== 'string') {
            return 0;
        }

        var decs = header.match(/\d/g);
        return decs ? Math.min.apply(null, decs) : 1;
    };

    var createList = function (wrapper, count) {
        while (count--) {
            wrapper = wrapper.appendChild(
                document.createElement('ol')
            );

            if (count) {
                wrapper = wrapper.appendChild(
                    document.createElement('li')
                );
            }
        }

        return wrapper;
    };

    var jumpBack = function (currentWrapper, offset) {
        while (offset--) {
            currentWrapper = currentWrapper.parentElement;
        }

        return currentWrapper;
    };

    var setAttrs = function (overwrite, prefix) {
        return function (src, target, index) {
            var content = src.textContent;
            var pre = prefix + '-' + index;
            target.textContent = content;

            var id = overwrite ? pre : (src.id || pre);

            id = encodeURIComponent(id);

            src.id = id;
            target.href = '#' + id;
        };
    };

    var buildTOC = function (options) {
        var selector = options.selector;
        var scope = options.scope;

        var ret = document.createElement('ol');
        var wrapper = ret;
        var lastLi = null;

        var _setAttrs = setAttrs(options.overwrite, options.prefix);

        getHeaders(selector, scope).reduce(function (prev, cur, index) {
            var currentLevel = getLevel(cur.tagName);
            var offset = currentLevel - prev;

            if (offset > 0) {
                wrapper = createList(lastLi, offset);
            }

            if (offset < 0) {
                wrapper = jumpBack(wrapper, -offset * 2);
            }

            wrapper = wrapper || ret;

            var li = document.createElement('li');
            var a = document.createElement('a');

            _setAttrs(cur, a, index);

            wrapper.appendChild(li).appendChild(a);

            lastLi = li;

            return currentLevel;
        }, getLevel(selector));

        return ret;
    };

    var initTOC = function (options) {
        var defaultOpts = {
            selector: 'h1, h2, h3, h4, h5, h6',
            scope: 'body',
            overwrite: false,
            prefix: 'toc'
        };

        options = extendObj(defaultOpts, options);

        var selector = options.selector;

        if (typeof selector !== 'string') {
            throw new TypeError('selector must be a string');
        }

        if (!selector.match(/^(?:h[1-6],?\s*)+$/g)) {
            throw new TypeError('selector must contains only h1-6');
        }

        return buildTOC(options);
      };

    // Generating the TOC 
    var container = document.querySelector('#toc');

    var toc = initTOC({
        selector: 'h1, h2',
        scope: '#doc-content', // you can specify here a tag where to look at 
        overwrite: false,
        prefix: 'toc'
    });

    container.appendChild(toc);
  }

Comments

0

You're in luck, and should be able to use angular.element almost exactly the same way as you would use jQuery.

angular.element('h1') will find h1 elements.

1 Comment

The problem is: I could find all the headlines, but how would I determine the correct order of them?
0

Angular jqLite is your option here, but don't forget that .find() is limited to lookups by tag name.

Check these SO SO answers if that helps.

1 Comment

does not answer the question, because it does not deal with retrieving the order of the headlines.
0

You can find the elements having the class ".text" using

angular.element(document.querySelectorAll(".text"));

1 Comment

It does not fully answer my question, but the document.querySelectorAll brought me to the right path. I can do element.querySelectorAll('h1,h2,h3,h4,h5,h6') to solve my problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.