Roy Prins
Roy Prins

Reputation: 3080

Find occurrences -not yet enclosed by A tags- and surround by A tags

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:

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 = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': '&quot;',
    "'": '&#39;',
    "/": '&#x2F;'
};

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;
        }

Upvotes: 2

Views: 175

Answers (2)

plalx
plalx

Reputation: 43728

I crafted a little solution for you. I think this is what you want:

HTML SAMPLE:

<div id="glossary">
    <dt>test</dt>
    <dt>Testing</dt>
    <dt>something</dt>
    <dt>some other thing</dt>
    <dt>long test</dt>
</div>

<div id="article">
    this is a test, to see if Testing will correctly be wrapped by something, or some other thing.
    This <a data-glossary-term href="#test">test</a> should not be wrapped again however, <span>something</span> should!
    Very long test should also be wrapped first.
</div>

JS:

$(function () {
    var $article = $('#article'),
        //clone to avoid multiple DOM reflows
        $clone = $article.clone(),
        //create a regex that matches all glossary terms (not case-sensitive)
        rx = new RegExp('\\b(' + $('#glossary dt').map(function () {
            return $(this).text();
        }).get().sort(function (term1, term2) {
            return term2.length - term1.length;
        }).join('|') + ')\\b', 'ig');

    //unwrap previously wrapped glossary terms to avoid issues
    //this would not be needed if your initial markup doesn't have terms wrapped already
    $clone.find('[data-glossary-term]').each(function () {
        $(this).replaceWith(this.childNodes);
    });

    //wrap terms in the 'a' tag
    $clone.html($clone.html().replace(rx, '<a data-glossary-term="$1" href="#$1">$1</a>'));

    $article.replaceWith($clone);
});

Upvotes: 2

yuvi
yuvi

Reputation: 18437

Instead of collecting the dt text into the array, collect the dom items themselves, then create a function like this:

function add_tags(td) {
    var dd = td.next(),
        q = td.text()
        txt = dd.text().replace(q, '<a href="#">'+q+'</a>');

    dd.html(txt);
};

And loop it. You'd probably want to add some check function to see if there are already any a-tags surrounding your word, but that's a good place to start

Upvotes: 0

Related Questions