1

I'm trying to make a basic web application where people can highlight multiple words (i.e., click on the first word, then click on a word further on, and everything will be highlighted, even on another line).

So far I was able to wrap all of the words in tags and set a click event listener to each one which changes it's className to "highlight" which is just background-color:yellow.

The problem is that only the background of that individual word is highlighted, but i want everything in between the two (or more, even on different lines) words to be highlighted.

To complicate things a little more, i also have punctuation and maybe other stuff inbetween the words, which are not surrounded by span tags, but I want everything between the words to have a different background color/ be selected.

I was thinking of just putting the necessary words that are selected in they're own, separate span tag, but then, I'm not sure how to make it dynamically change exactly, and i also want the user to save the selections and then re-select them with a button or something, so that means that one word could be in 2 different phrases, and I'm not sure how one word could be in two different span tags....

So basically: how can I select multiple words in JavaScript including highlighting everything inbetween the two words?

EDIT There was a request for some of the code I've tried, so I've attempted to simplify the relevant sections:

var h= eid("HebrewS");
var currentPhrase=[];
var equal=false;
var shtikles = [

];

h.innerHTML = h.innerHTML.replace(/([\u0590-\u05FF\"\']+)/g,'<span class="shtikle""">$1</span>');

var words = q("#HebrewS span");
words.forEach(function(item, idx){


shtikles[idx] = {obj:item, id:idx, heb:item.innerHTML, translation:"means "+ idx};
item.addEventListener("click", function(e) {


if(currentPhrase.length == 0) {
    currentPhrase.push(idx);
    currentPhrase[1]=idx;
    equal=true;
}
else {
    currentPhrase[1]=idx;

    if(currentPhrase[1] < currentPhrase[0]) {
        currentPhrase.reverse();
    }
    if(currentPhrase[0]==currentPhrase[1]) 
        if(!equal) {
            equal=true;
        } else {
            currentPhrase = new Array();
            equal=false;
        }
    else 
        equal=false;

    }

selectPhrase(currentPhrase);

});

function selectPhrase(p) {

for(var i =0;i<shtikles.length;i++) {
    if(shtikles[i].obj)
    if(p.length > 0) {
    if(i  < p[0] || i > p[1]) {
        if(shtikles[i].obj.className != "shtikle") {

            shtikles[i].obj.className ="shtikle";
        }
    }
    } else {
        shtikles[i].obj.className = "shtikle";
    }
} 
for(var i = p[0]; i <= p[1]; i++) {
    shtikles[i].obj.className="phrasePart";
}
}

function q(a) {
return document.querySelectorAll(a);

}
function eid(id) {
return document.getElementById(id);

}

Now for the html:

<div style="" id ="HebrewS">today I will show,.  you how] to read.. {Maamarim! וחזקת והיית לאיש1, הנה ידוע2 שהמאמר שאמר אדמו"ר (מהורש"ב) נ"ע ביום השביעי3 דחגיגת הבר מצוה של בנו יחידו כ"ק מו"ח אדמו"ר, י"ט תמוז4 תרנ"ג [שמאמר זה הוא סיום וחותם ההמשך תפילין דמארי עלמא5 שהתחיל לומר בי"ב תמוז, יום הבר מצוה] היתה התחלתו בפסוק זה. – השייכות דפסוק זה (וחזקת והיית לאיש) לבר מצוה בפשטות היא, ע"פ הידוע6 דזה שבן שלש עשרה (דוקא) מחוייב במצוות הוא כי אז דוקא נק' בשם איש. וצריך להבין, דמכיון שבן י"ג שנה נעשה איש (ע"פ טבע), מהי ההדגשה לומר (בחגיגת בר מצוה) וחזקת והיית לאיש. וגם צריך להבין, הרי המעלה דבן י"ג שנה היא שאז נעשה בר דעת7, דדעת הוא במוחין, ובפרט לפי המבואר בהמאמר ד"ה איתא במדרש תילים תרנ"ג [שהוא אחד המאמרים שחזר אותם כ"ק מו"ח אדמו"ר בחגיגת הבר  שלו]8 שהמעלה דבן י"ג שנה היא שאז יש לו עצם המוחין9, ומהו הדיוק בבן י"ג שנה בהתואר איש שמורה10 על המדות

Now css:

<style type="text/css">

.shtikle:hover{
    background-color:yellow;
}

.phrasePart{
    background-color: purple;
    border: 0px solid black;
}

</style>

I haven't tested the simplified version of the code, but if you try it out should work.

The basic point is :it selects each word individually, but doesn't highlight the stuff between the words (and I don't want to put all of the words in the current phrase into they're own span, because I want to save the phrase and have it selectable later, and also with multiple phrases some words might be in both)

1 Answers1

0

Split everything into single nodes and then work with ranges. Simple implementation might look like below. It works on two clicks (right click removes selections) and is ready to implement click and slide (you'll have to implement mouseenter event listener with some boolean flag). So the point is that after every click id of each node is checked and either it becomes starting point of a range or closes the range with a for loop that adds class for every node in between.

You might then store id ranges somewhere there and activate them on eg. button click.

//EDIT

check edit below, this is totally messy, but I believe it should fit your needs.

const txt = 'this is some word in a wordy place where all words are kind of, well... this is some word in a wordy place where all words are kind of something so: this is some word in a wordy place where all words are kind of, and then -> this is some word in a wordy place where all words are kind of nothing';
let startIndex = null;
let lines = document.querySelector('.lines');
lines.innerHTML = txt.split(' ').map((z, i) => {

  return (z.replace(new RegExp("\\w+|\\W+", "g"), (t) => { return /\w+/.test(t) ? `<span data-id="${i}" data-word>${t}</span>` : `<span data-id="${i}">${t}</span>` }));
}).join(' ');
nodes = Array.prototype.slice.call(document.querySelectorAll('.lines span'));
nodes.forEach(z => {
 if(z.hasAttribute('data-word')) {
    z.addEventListener('mousedown', (e) => {
   const id = Number(e.target.getAttribute('data-id'));
   if (startIndex === null) {
     startIndex = id;
     e.target.classList.add('active');
    } else {
     const range = id > startIndex ? [startIndex, id] : [id, startIndex];
      for(let i = range[0]; i<= range[1]; i++) {
       (Array.prototype.slice.call(document.querySelectorAll('span[data-id="' + i + '"]'))).forEach(e => e.classList.add('active'));
      };
      startIndex = null;
    }
  });
  }
});

window.oncontextmenu = function ()
{
    startIndex = null;
    nodes.forEach(z => z.classList.remove('active'))
    return false;
}
.lines {
  user-select: none;
}
.lines span {
  display: inline-block;
  padding:3px;
  box-decoration-break: clone;
  transition:.2s;
}
.lines span.active { 
  background: salmon;
  box-shadow: 3px 0 0 salmon, -3px 0 0 salmon;
}
[data-word] {
  cursor:pointer;
}
<div class="lines"></div>
Maciej Kwas
  • 6,169
  • 2
  • 27
  • 51
  • Well actually, just testing the code snippet, it appears that you can actually select the punctuation also, like the periods and colons etc., but I want the user to only select the actual words, but nevertheless highlight the punctuation. Try testing the example code to see what I mean, it might just be an easy fix – B''H Bi'ezras -- Boruch Hashem Jul 05 '18 at 06:03
  • @user2016831 I've made an edit to the answer splitting words and other signs with regexp- have a look. – Maciej Kwas Jul 06 '18 at 13:06
  • cool thanks so basically you just checked if the given expression fits the edges and add the data-word class to it then only add the click events to those elements, but you also wrapped everything else in span I think.. So how would I do the same with Hebrew, just replacing the text on new Regex with the Hebrew set like the code in the question or do I also have to replace the .test part, and if so with what? – B''H Bi'ezras -- Boruch Hashem Jul 08 '18 at 02:41
  • for example my current regex is /([\u0590-\u05FF\"\']+)/g, for Hebrew, what part of that would I use for the .test part? – B''H Bi'ezras -- Boruch Hashem Jul 08 '18 at 02:46
  • Basically what I do is: Match `Word` *OR* `Not a word` and then if it *IS* a `word`, add specific data attribute. In your case all you have to do is to add a proper negation, so Match `unicode range` or `everything except unicode range` and if it is `unicode range` add proper attr. – Maciej Kwas Jul 08 '18 at 21:13
  • thanks I got the basic idea but I'm trying the replace part and when I put in ${s} it doesn't replace it like it does in yours:var reg= /([\u0590-\u05FF\"\']+)/g; h.innerHTML = Html.split(' ').map((z, i) => { return z.replace(reg,(s)=>{return '${s}'})}).join(' '); It's not even doing anything special, just testing, but it doesn't replace the variable in mine like it does in yours..? – B''H Bi'ezras -- Boruch Hashem Jul 09 '18 at 04:26
  • Thanks but the replace doesn't work with Hebrew, when I have English words with punctuation it separates the punctuation and word into 2 separate spans, but with Hebrew and punctuation it keeps it all in the same span: var reg= /([\u0590-\u05FF\"\']+)/; h.innerHTML = Html.split(' ').map((z, i) => { return z.replace(new RegExp("\\w+|\\W+", "g"),(s)=>{var strB = '' +s +''; return strB;})}).join(' '); alert(h.innerHTML); – B''H Bi'ezras -- Boruch Hashem Jul 09 '18 at 05:32
  • And when I add the unicode range in the new RegExp part new RegExp("[\u0590-\u05FF\"\']+|^[\u0590-\u05FF\"\']+", "g"). It adds only the Hebrew characters to the span and not the punctuation, so it's back where I started. I think it's something to do with the .test part, what should I put there exactly? – B''H Bi'ezras -- Boruch Hashem Jul 09 '18 at 05:40
  • @user2016831 have a look at this: ```lines.innerHTML = txt.split(' ').map((z, i) => { return (z.replace(new RegExp("([\u0590-\u05FF]+)|([^\u0590-\u05FF]+)", "g"), (t) => { return /[\u0590-\u05FF]+/.test(t) ? `${t}` : `${t}` })); }).join(' ');``` – Maciej Kwas Jul 10 '18 at 09:04
  • thanks that actually worked! But I also want to somewhat understand the code, why does it work if you check "in unicode range" or not or instead check " is a word or not", or just include everythig initially, shouldnt it all be the same? – B''H Bi'ezras -- Boruch Hashem Jul 17 '18 at 23:09
  • @user2016831 You just want split all words into parts. You could split every single letter by letter, check it and eventually you would end up with with every single sign to be parametrized whether it fits the unicode range or not. Above code do the same, but it splits whole parts of signs rather than single signs, so `hello...` is splitted into `["hello", "..."]` and transformed into `["hello", "..."]` – Maciej Kwas Jul 18 '18 at 07:54