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.

HannesH
  • 1,024
  • 1
  • 11
  • 16

5 Answers5

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>
HannesH
  • 1,024
  • 1
  • 11
  • 16
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);
  }
palamunder
  • 2,555
  • 1
  • 19
  • 20
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.

Mikke
  • 2,167
  • 16
  • 22
  • The problem is: I could find all the headlines, but how would I determine the correct order of them? – HannesH Feb 25 '14 at 09:25
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.

Community
  • 1
  • 1
Yagiz Ozturk
  • 5,408
  • 7
  • 29
  • 44
  • does not answer the question, because it does not deal with retrieving the order of the headlines. – HannesH Feb 25 '14 at 15:45
0

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

angular.element(document.querySelectorAll(".text"));
V31
  • 7,626
  • 3
  • 26
  • 44
  • 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. – HannesH Feb 25 '14 at 09:27