[AngularJS] Add Tooltip to Every Word


Add tooltip to every word via AngularJS.

Demo

Real world application is Pāli Tipiṭaka.

Source code:

index.html | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!doctype html>
<html ng-app="demoWrapEveryWord">
<head>
  <meta charset="utf-8">
  <title>AngularJS Wrap Every Words Demo</title>
  <link rel="stylesheet" type="text/css" href="tooltip.css">
</head>
<body>

<div wrap-every-word>
<p>1. Tena samayena buddho bhagavā verañjāyaṃ viharati naḷerupucimandamūle mahatā bhikkhusaṅghena saddhiṃ pañcamattehi bhikkhusatehi. Assosi kho verañjo brāhmaṇo – ‘‘samaṇo khalu, bho, gotamo sakyaputto sakyakulā pabbajito verañjāyaṃ viharati naḷerupucimandamūle mahatā bhikkhusaṅghena saddhiṃ pañcamattehi bhikkhusatehi. Taṃ kho pana bhavantaṃ gotamaṃ evaṃ kalyāṇo kittisaddo abbhuggato – ‘itipi so bhagavā arahaṃ sammāsambuddho vijjācaraṇasampanno sugato lokavidū anuttaro purisadammasārathi satthā devamanussānaṃ buddho bhagavā [bhagavāti (syā.), dī. ni. 1.157, abbhuggatākārena pana sameti]. So imaṃ lokaṃ sadevakaṃ samārakaṃ sabrahmakaṃ sassamaṇabrāhmaṇiṃ pajaṃ sadevamanussaṃ sayaṃ abhiññā sacchikatvā pavedeti. So dhammaṃ deseti ādikalyāṇaṃ majjhekalyāṇaṃ pariyosānakalyāṇaṃ sātthaṃ sabyañjanaṃ; kevalaparipuṇṇaṃ parisuddhaṃ brahmacariyaṃ pakāseti; sādhu kho pana tathārūpānaṃ arahataṃ dassanaṃ hotī’’’ti.</p>

<p>2.[ito paraṃ yāva pārā. 15-16 padakkhiṇaṃ katvā pakkāmīti pāṭho a. ni. 8.11] Atha kho verañjo brāhmaṇo yena bhagavā tenupasaṅkami; upasaṅkamitvā bhagavatā saddhiṃ sammodi. Sammodanīyaṃ kathaṃ sāraṇīyaṃ vītisāretvā ekamantaṃ nisīdi . Ekamantaṃ nisinno kho verañjo brāhmaṇo bhagavantaṃ etadavoca – ‘‘sutaṃ metaṃ, bho gotama – ‘na samaṇo gotamo brāhmaṇe jiṇṇe vuḍḍhe mahallake addhagate vayoanuppatte abhivādeti vā paccuṭṭheti vā āsanena vā nimantetī’ti. Tayidaṃ, bho gotama, tatheva? Na hi bhavaṃ gotamo brāhmaṇe jiṇṇe vuḍḍhe mahallake addhagate vayoanuppatte abhivādeti vā paccuṭṭheti vā āsanena vā nimanteti? Tayidaṃ, bho gotama, na sampannamevā’’ti.</p>

<p>‘‘Nāhaṃ taṃ, brāhmaṇa, passāmi sadevake loke samārake sabrahmake sassamaṇabrāhmaṇiyā pajāya sadevamanussāya yamahaṃ abhivādeyyaṃ vā paccuṭṭheyyaṃ vā āsanena vā nimanteyyaṃ. Yañhi, brāhmaṇa, tathāgato abhivādeyya vā paccuṭṭheyya vā āsanena vā nimanteyya, muddhāpi tassa vipateyyā’’ti.</p>

<p>3. ‘‘Arasarūpo bhavaṃ gotamo’’ti? ‘‘Atthi khvesa, brāhmaṇa, pariyāyo yena maṃ pariyāyena sammā vadamāno vadeyya – ‘arasarūpo samaṇo gotamo’ti. Ye te, brāhmaṇa, rūparasā saddarasā gandharasā rasarasā phoṭṭhabbarasā te tathāgatassa pahīnā ucchinnamūlā tālāvatthukatā anabhāvaṃkatā [anabhāvakatā (sī.) anabhāvaṃgatā (syā.)] āyatiṃ anuppādadhammā. Ayaṃ kho, brāhmaṇa, pariyāyo yena maṃ pariyāyena sammā vadamāno vadeyya – ‘arasarūpo samaṇo gotamo’ti, no ca kho yaṃ tvaṃ sandhāya vadesī’’ti.</p>
</div>

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="tooltip.js"></script>
<script src="ngWrapper.js"></script>
<script src="app.js"></script>
</body>
</html>
app.js | repository | view raw
1
2
3
4
5
6
7
'use strict';


angular.module('demoWrapEveryWord', ['tooltip', 'wordWrapper']).
  run([function() {
    // nothing to do
  }]);
ngWrapper.js | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
'use strict';


angular.module('wordWrapper', ['tooltip']).
  directive('wrapEveryWord', ['wrapStringOfWords', function(wrapStringOfWords) {
    /**
     * wrap every word in ELEMENT and add tooltip to every word.
     */
    return {
      restrict: 'A',
      link: function(scope, elm, attrs) {
        wrapStringOfWords.traverse(elm[0])
      }
    };
  }]).

  factory('wrapStringOfWords', ['processStringOfWords', function(processStringOfWords) {
    /**
     * find all words in the element
     * @param {DOM element}
     */
    function traverse(elm) {
      // 1: element node
      if (elm.nodeType == 1) {
        for (var i=0; i<elm.childNodes.length; i++)
          // recursively call self to process
          traverse(elm.childNodes[i]);
        return;
      }

      // 3: text node
      if (elm.nodeType == 3) {
        var string = elm.nodeValue;
        if (string.replace(/\s*/, '') !== '')
          // string is not whitespace
          elm.parentNode.replaceChild(processStringOfWords.toDom(string), elm);
        return;
      }
    }

    var serviceInstance = { traverse: traverse };
    return serviceInstance;
  }]).

  factory('processStringOfWords', ['AddTooltipToElement', function(AddTooltipToElement) {

    function toDom(string) {
      // wrap all words in span
      var spanContainer = htmlStr2Dom(markInSpan(string));

      // add tooltip to every word
      for (var i=0; i<spanContainer.childNodes.length; i++) {
        var node = spanContainer.childNodes[i];
        var tagName = node.tagName;
        if (tagName && tagName.toLowerCase() === 'span') {
          AddTooltipToElement.add(node)
        }
      }
      return spanContainer;
    }

    function htmlStr2Dom(string) {
      // @see http://stackoverflow.com/questions/3103962/converting-html-string-into-dom-elements
      // @see http://stackoverflow.com/questions/888875/how-to-parse-html-from-javascript-in-firefox
      var spanContainer = document.createElement('span');
      spanContainer.innerHTML = string;
      return spanContainer;
    }

    function markInSpan(string) {
      return string.replace(/[AaBbCcDdEeGgHhIiJjKkLlMmNnOoPpRrSsTtUuVvYyĀāĪīŪūṀṁṂṃŊŋṆṇṄṅÑñṬṭḌḍḶḷ]+/g, '<span>$&</span>');
    }

    var serviceInstance = { toDom: toDom };
    return serviceInstance;
  }]);
tooltip.js | repository | view raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
'use strict';

/* Services */


angular.module('tooltip', []).
  factory('tooltip', ['$rootScope', '$compile', function($rootScope, $compile) {
    // init tooltip
    var scope = $rootScope.$new(true);
    var isMouseInTooltip = false;
    scope.onmouseenter = function() {
      // mouse enters tooltip
      isMouseInTooltip = true;
    };
    scope.onmouseleave = function() {
      // mouse leaves tooltip
      isMouseInTooltip = false;
      hide();
    };
    var _left = 0;
    var _top = 0;

    var tooltip = $compile('<div class="tooltip" ng-mouseenter="onmouseenter()" ng-mouseleave="onmouseleave()"></div>')(scope);
    tooltip.css('max-width', viewWidth() + 'px');

    // append tooltip to the end of body element
    angular.element(document.getElementsByTagName('body')[0]).append(tooltip);


    function viewWidth() {
      // FIXME: why -32 here?
      return (window.innerWidth || document.documentElement.clientWidth) - 32;
    }

    function setContent(content) {
      tooltip.children().remove();
      if (angular.isUndefined(content)) {
        throw 'In tooltip: content undefined!';
      } else if (angular.isString(content)) {
        tooltip.html(content);
      } else {
        tooltip.append(content);
      }
    }

    function setPosition(position) {
      _left = parseInt(position.left.replace('px', ''));
      _top = parseInt(position.top.replace('px', ''));
    }

    function show() {
      // move tooltip to the right
      // (don't cross the right side of browser inner window)
      var _right = _left + tooltip.prop('offsetWidth');
      if ( _right > viewWidth() )
        _left -= (_right - viewWidth());

      tooltip.css('left', _left + 'px');
      tooltip.css('top', _top + 'px');
    }

    function hide() {
      if (!isMouseInTooltip)
        tooltip.css('left', '-9999px');
    }

    var serviceInstance = {
      viewWidth: viewWidth,
      isHidden: function() {return (tooltip.css('left') === '-9999px');},
      getLeftSpace: function() {return _left;},
      getRightSpace: function() {return viewWidth() - _left - tooltip.prop('offsetWidth');},
      setContent: setContent,
      setPosition: setPosition,
      show: show,
      hide: hide
    };
    return serviceInstance;
  }]).

  factory('AddTooltipToElement', ['tooltip', function(tooltip) {
    // when user's mouse hovers over words,
    // delay a period of time before look up.
    var DELAY_INTERVAL = 1000; // ms

    var isMouseInWord = false;

    function onWordMouseOver(e) {
      isMouseInWord = true;
      this.style.color = 'red';

      setTimeout(angular.bind(this, function() {
        // 'this' keyword here refers to raw dom element
        if (this.style.color === 'red') {
          // mouse is still on word
          var word = this.innerHTML.toLowerCase();
          tooltip.setContent(
            word + ' ' + word + '<br>' +
            '<span>' + word + '</span>' + ' ' + word
          );
          tooltip.setPosition({
            'left': (this.getBoundingClientRect().left + 'px'),
            'top': (this.getBoundingClientRect().top + this.offsetHeight + 'px'),
          });
          tooltip.show();
        }
      }), DELAY_INTERVAL);
    }

    function onWordMouseOut(e) {
      isMouseInWord = false;
      this.style.color = '';

      setTimeout(angular.bind(this, function() {
        if (!isMouseInWord) {
          tooltip.hide();
        }
      }), DELAY_INTERVAL);
    }

    function add(elm) {
      elm.onmouseover = onWordMouseOver;
      elm.onmouseout = onWordMouseOut;
    }

    var serviceInstance = { add: add };
    return serviceInstance;
  }]);
tooltip.css | repository | view raw
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * keyword: css long word force line break
 * http://webdesignerwall.com/tutorials/word-wrap-force-text-to-wrap
 */
.tooltip {
  position: absolute;
  left: -9999px;
  background-color: #CCFFFF;
  border-radius: 10px;
  font-family: Tahoma, Arial, serif;
  word-wrap: break-word;
}

Tested on: Chromium Version 50.0.2661.102 Ubuntu 16.04 (64-bit), AngularJS 1.5.5.


References:

[1][AngularJS] Tooltip