web-dev-qa-db-fra.com

Détecter quel mot a été cliqué dans un texte

Je construis un script JS qui, à un moment donné, peut, sur une page donnée, permettre à l’utilisateur de cliquer sur n’importe quel mot et de le stocker dans une variable.

J'ai une solution assez moche qui implique l'analyse de classe à l'aide de jQuery: J'analyse d'abord le code HTML entier, je scinde tout le contenu de chaque espace " " et rajoute tout ce qui est enveloppé dans un <span class="Word">Word</span> , puis j'ajoute un événement avec jQ pour détecter les clics sur une telle classe, et en utilisant $ (this) .innerHTML, je reçois le mot cliqué.

C'est lent et moche à bien des égards et j'espérais que quelqu'un connaisse un autre moyen d'y parvenir.

PS: Je pourrais envisager de l’exécuter en tant qu’extension de navigateur. Si cela ne vous semble pas possible avec JS uniquement, et si vous connaissez une API de navigateur qui le permettrait, n'hésitez pas à le mentionner!

Une solution possible consiste à amener l'utilisateur à mettre en surbrillance le mot au lieu de cliquer dessus, mais j'aimerais vraiment pouvoir réaliser la même chose en un seul clic!

37
Cystack

Voici une solution qui fonctionnera sans ajouter des tonnes de plages au document (fonctionne sur Webkit, Mozilla et IE9 +):

http://jsfiddle.net/Vap7C/15/

<p class="clickable">some words</p>

$(".clickable").click(function(e) {
    s = window.getSelection();
    var range = s.getRangeAt(0);
    var node = s.anchorNode;
    while (range.toString().indexOf(' ') != 0) {
        range.setStart(node, (range.startOffset - 1));
    }
    range.setStart(node, range.startOffset + 1);
    do {
        range.setEnd(node, range.endOffset + 1);

    } while (range.toString().indexOf(' ') == -1 && range.toString().trim() != '' && range.endOffset < node.length);
    var str = range.toString().trim();
    alert(str);
});​

dans IE8, il y a des problèmes à cause de getSelection. Ce lien ( Existe-t-il une solution multi-navigateurs pour getSelection ()? ) qui peut aider à résoudre ces problèmes. Je n'ai pas testé sur Opera.

J'ai utilisé http://jsfiddle.net/Vap7C/1/ / à partir d'une question similaire comme point de départ. Il a utilisé la fonction Selection.modify :

s.modify('extend','forward','Word');
s.modify('extend','backward','Word');

Malheureusement, ils ne comprennent pas toujours la totalité de la Parole. En guise de solution de contournement, j'ai obtenu le Range pour la sélection et ajouté deux boucles pour trouver les limites du mot. Le premier continue à ajouter des caractères au mot jusqu'à ce qu'il atteigne un espace. la deuxième boucle va à la fin de la Parole jusqu'à atteindre un espace. 

Cela prendra également toute ponctuation à la fin du mot, alors assurez-vous de le supprimer si vous en avez besoin.

47
stevendaniels

Autant que je sache, l'ajout d'une span pour chaque mot est le seul moyen de le faire.

Vous pourriez envisager d’utiliser Lettering.js , qui gère le fractionnement pour vous. Bien que cela n’affectera pas vraiment les performances, à moins que votre "code de scission" soit inefficace.

Ensuite, au lieu de lier .click() à chaque span, il serait plus efficace de lier une seule .click() au conteneur de spans et de vérifier event.target pour voir quelle span a été cliquée.

13
thirtydot

À ma connaissance, la seule façon de naviguer entre plusieurs navigateurs (IE <8) consiste à envelopper des éléments span. C'est moche mais pas vraiment si lent. 

Cet exemple provient directement de la documentation de la fonction jQuery .css (), mais avec un bloc de texte énorme à prétraiter:

http://jsfiddle.net/kMvYy/

Voici une autre façon de le faire (donné ici: jquery capture la valeur Word ) sur le même bloc de texte qui ne nécessite pas d’emballage dans span. http://jsfiddle.net/Vap7C/ 1

4
spike

-EDIT- Qu'en est-il? il utilise getSelection() lié à mouseup

<script type="text/javascript" src="jquery-1.6.3.min.js"></script>
<script>
$(document).ready(function(){
    words = [];
    $("#myId").bind("mouseup",function(){
        Word = window.getSelection().toString();
        if(Word != ''){
            if( confirm("Add *"+Word+"* to array?") ){words.Push(Word);}
        }
    });
    //just to see what we've got
    $('button').click(function(){alert(words);});
});
</script>

<div id='myId'>
    Some random text in here with many words huh
</div>
<button>See content</button>

Je ne peux pas penser à un moyen de diviser, c'est ce que je ferais, un petit plugin qui se scinde en spans et lorsque vous cliquez dessus, il ajoutera son contenu à array pour une utilisation ultérieure:

<script type="text/javascript" src="jquery-1.6.3.min.js"></script>
<script>
//plugin, take it to another file
(function( $ ){
$.fn.splitWords = function(ary) {
    this.html('<span>'+this.html().split(' ').join('</span> <span>')+'</span>');
    this.children('span').click(function(){
        $(this).css("background-color","#C0DEED");
        ary.Push($(this).html());
    });
};
})( jQuery );
//plugin, take it to another file

$(document).ready(function(){
    var clicked_words = [];
    $('#myId').splitWords(clicked_words);
    //just to see what we've stored
    $('button').click(function(){alert(clicked_words);});
});
</script>

<div id='myId'>
    Some random text in here with many words huh
</div>
<button>See content</button>
3
derp

Voici des améliorations pour la réponse acceptée:

$(".clickable").click(function (e) {
    var selection = window.getSelection();
    if (!selection || selection.rangeCount < 1) return true;
    var range = selection.getRangeAt(0);
    var node = selection.anchorNode;
    var Word_regexp = /^\w*$/;

    // Extend the range backward until it matches Word beginning
    while ((range.startOffset > 0) && range.toString().match(Word_regexp)) {
      range.setStart(node, (range.startOffset - 1));
    }
    // Restore the valid Word match after overshooting
    if (!range.toString().match(Word_regexp)) {
      range.setStart(node, range.startOffset + 1);
    }

    // Extend the range forward until it matches Word ending
    while ((range.endOffset < node.length) && range.toString().match(Word_regexp)) {
      range.setEnd(node, range.endOffset + 1);
    }
    // Restore the valid Word match after overshooting
    if (!range.toString().match(Word_regexp)) {
      range.setEnd(node, range.endOffset - 1);
    }

    var Word = range.toString();
});​
2
link0ff

Voici une méthode complètement différente. Je ne suis pas sûr de son aspect pratique, mais cela peut vous donner des idées différentes ..__ Voici ce que je pense si vous avez une balise conteneur avec une position relative avec juste du texte. Ensuite, vous pouvez placer une étendue autour de chaque enregistrement Word avec son décalage Hauteur, Largeur, Gauche et Haut, puis supprimer l’étendue. Enregistrez-les dans un tableau puis, quand il y a un clic dans la zone, faites une recherche pour trouver quel mot était le plus proche du clic. Ce serait évidemment intensif au début. Donc, cela fonctionnerait mieux dans une situation où la personne passera du temps à parcourir l'article. L'avantage est que vous n'avez pas à vous soucier de centaines d'éléments supplémentaires, mais que cet avantage peut être au mieux marginal.

Notez que je pense que vous pouvez supprimer l'élément conteneur du DOM pour accélérer le processus tout en obtenant les distances de décalage, mais je ne suis pas positif.

1
qw3n
1
Alon Eitan

Et une autre prise sur la réponse de @ stevendaniel:

$('.clickable').click(function(){
   var sel=window.getSelection();
   var str=sel.anchorNode.nodeValue,len=str.length, a=b=sel.anchorOffset;
   while(str[a]!=' '&&a--){}; if (str[a]==' ') a++; // start of Word
   while(str[b]!=' '&&b++<len){};                   // end of Word+1
   console.log(str.substring(a,b));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<p class="clickable">The objective can also be achieved by simply analysing the
string you get from <code>sel=window.getSelection()</code>. Two simple searches for
the next blank before and after the Word, pointed to by the current position
(<code>sel.anchorOffset</code>) and the work is done:</p>

<p>This second paragraph is <em>not</em> clickable. I tested this on Chrome and Internet Explorer (IE11)</p>

0
cars10m

Ceci est une suite de mon commentaire à réponse de stevendaniels (ci-dessus):

Dans la première section de code ci-dessus, range.setStart (node, (Range.startOffset - 1)); se bloque lorsqu’il est exécuté sur le premier mot dans un "noeud", car il tente de définir la plage sur une valeur négative. J'ai essayé ajouter de la logique pour empêcher cela, mais ensuite la suivante range.setStart (noeud, range.startOffset + 1); renvoie tout sauf le premier lettre du premier mot. De plus, lorsque les mots sont séparés par une nouvelle ligne, le dernier mot de la ligne précédente est renvoyé en plus du cliqué sur Word. Donc, cela nécessite du travail.

Voici mon code pour que le code d'expansion de la plage dans cette réponse fonctionne de manière fiable:

while (range.startOffset !== 0) {                   // start of node
    range.setStart(node, range.startOffset - 1)     // back up 1 char
    if (range.toString().search(/\s/) === 0) {      // space character
        range.setStart(node, range.startOffset + 1);// move forward 1 char
        break;
    }
}

while (range.endOffset < node.length) {         // end of node
    range.setEnd(node, range.endOffset + 1)     // forward 1 char
    if (range.toString().search(/\s/) !== -1) { // space character
        range.setEnd(node, range.endOffset - 1);// back 1 char
        break;
    }
}
0
CODE-REaD

Ce qui ressemble à une solution légèrement plus simple. 

document.addEventListener('selectionchange', () => {
  const selection = window.getSelection();
  const matchingRE = new RegExp(`^.{0,${selection.focusOffset}}\\s+(\\w+)`);
  const clickedWord = (matchingRE.exec(selectiaon.focusNode.textContent) || ['']).pop();
});

Je teste

0
stringparser