I am trying to make good use of HTML definition lists, to serve as a glossary with tooltips. Please see this fiddle for the intended result (at the bottom).
If parts of the text in an article match the terms in the definition list, a tooltip bubble is shown with the matching definition. This is done by a surrounding A-tag. So far so good.
Now the question is how to parse the article so that all the parts that can be found in the glossary will get automatically surrounded by A-tags. The terms can be for instance:
- group
- social group
- group behavior
- research
The parsing has to be 'greedy' so it parses the longest definition first.
var arr = []; /* Array of terms, sorted by length */
$('#glossary dt').each( function() { arr.push($(this).text()); });
arr.sort(function(a,b) { return b.length - a.length; });
Next it has to surround occurrences of the first array-item in #article.html by A-tags, unless they are already within A-tags. Also this should be case-insensitive.
/* don't know how to approach this */
Finally move to the next array item and repeat.
/* ok, i can figure the loop out myself */
My problem is with checking if a certain string is already within A-tags and also with placing A-tags around a part of text. Replace should be avoided to keep the upper/lowercase the same. Check the JSfiddle for the intended result.
edit: Building on the solution of plalx below, I came up the following code:
/*
This script matches text strings in an article with terms in a defintion list (DL);
the DL acts as a glossary. The text strings are wrapped in '<span>' tags,
with the 'title' attribute containing the definition. This allows for easy custom tooltips.
- This script is 'greedy' the longest terms are matched first;
- This script preserves capitalization and escapes HTML in the definitions
*/
var $article = $('#container-inhoud');
var $terms = $('#glossary dt');
//clone to avoid multiple DOM reflows
var $clone = $article.clone();
//create a regex that matches all glossary terms (not case-sensitive), sorted by length
var rx = new RegExp('\\b(' + $terms.map(function () {
return $(this).text();
}).get().sort(function (term1, term2) {
return term2.length - term1.length;
}).join('|') + ')\\b', 'ig');
var entityMap = {
"&": "&",
"<": "<",
">": ">",
'"': '"',
"'": ''',
"/": '/'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
//wrap any text string that corresponds with a definition term in a 'span' tag
//the title contains the connected definition.
function replacer(match){
var definition = $terms.filter(function() {
return $(this).text().toLowerCase() == match.toLowerCase();
}).next('dd').text();
definition = escapeHtml(definition);
return '<span class="tooltip" title="' + definition + '">' + match + '</span>';
}
//call the replace function for every regex match
$clone.html($clone.html().replace(rx , replacer));
//unwrap the terms in the glossary section (to avoid tooltips within the glossary itself)
//only needed if the #glossary is within the #article container, otherwise delete the next line.
$clone.find('#glossary .tooltip').contents().unwrap();
$article.replaceWith($clone);
The following code displays the tooltips.
/*
This script displays a tooltip for every 'span' with class 'tooltip'.
The 'title' attribute will be displayed as the tooltip.
The tooltip is displayed directly above and in line with the left side of the element.
Any offsetting is done by manipulation the 'left' and 'top' attributes in the CSS.
*/
$("span.tooltip").hover(
function () {
var bubble = $("#bubble");
bubble.text($(this).attr('title'));
var ypos = $(this).offset().top - bubble.height();
var xpos = $(this).offset().left;
bubble.css({"left":xpos+"px","top":ypos+"px"});
bubble.show();
},
function () {
$("#bubble").hide();
}
);
I used to following CSS to format the tooltip bubble:
#bubble{
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
border: 1px solid #888;
color: #ee6c31;
background-color: rgba(255, 255, 255, 0.85);
position:absolute;
z-index:200;
padding:5px 8px;
margin: -15px 5px 5px -10px;
display:none;
}